Compare commits

..

98 Commits

Author SHA1 Message Date
Álison Fernandes
225cb24ac1 Add ViewAsQrCode first draft 2025-03-19 15:15:26 +00:00
David Perez
f4f669683e PM-19334: Propagate errors to the UI (#4893) 2025-03-18 17:27:41 +00:00
Dave Severns
475a82e0fb PM-19335 add throwable val to generator error results (#4894) 2025-03-18 17:08:11 +00:00
Phil Cappelli
f22156389b BWA-119 - Unable to Access Manual Code Entry After Denying Camera Permissions on (#4891) 2025-03-18 15:50:51 +00:00
Phil Cappelli
4f09f5dae4 PM-18872 - When a Folder name is long, the View/Edit Item > Folder selection screen doesn't adjust well (#4892) 2025-03-18 15:50:39 +00:00
David Perez
3934bc9ae2 PM-19314: Propagate remaining auth errors to the UI (#4888) 2025-03-18 15:24:17 +00:00
David Perez
72c9149d27 PM-19295: Propagate password errors to the UI (#4884) 2025-03-18 14:05:36 +00:00
David Perez
a040a38ce8 PM-19296: Propagate login errors to the UI (#4885) 2025-03-18 14:05:07 +00:00
Dave Severns
ef3b7730d0 PM-19289 propagating remaining vault result errors. (#4881) 2025-03-18 13:51:30 +00:00
David Perez
ad8d8d271a PM-19294: Propagate the Register errors to the UI (#4883) 2025-03-17 19:54:43 +00:00
David Perez
4954e57007 Update gem dependencies (#4882) 2025-03-17 19:29:22 +00:00
Dave Severns
6f50fffd17 PM-19275 propagate the errors for the vault unlock error result types (#4878) 2025-03-17 19:24:13 +00:00
David Perez
44c5755301 PM-19284: Propagate SSO flow errors to the UI (#4880) 2025-03-17 19:14:17 +00:00
David Perez
b20eece3aa PM-19283: Propagate error from email token and known device flows (#4879) 2025-03-17 17:47:07 +00:00
renovate[bot]
869a3b00a5 [deps]: Lock file maintenance (#4875)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 15:47:03 +00:00
David Perez
7f8e848c46 PM-19245: Propagate error from password validation to UI (#4877) 2025-03-17 15:36:01 +00:00
Dave Severns
6c784d28eb PM-19272 propagate errors from cipher results (#4876) 2025-03-17 15:18:05 +00:00
Dave Severns
dfcdc72499 PM-19241 folder result errors propagated to UI (#4870) 2025-03-17 12:20:44 +00:00
Dave Severns
db287ddce5 PM-19243 send result errors propagated to UI (#4872) 2025-03-14 22:01:42 +00:00
David Perez
9ea85917b1 PM-19239: Propagate delete account errors to the UI (#4871) 2025-03-14 21:27:56 +00:00
Dave Severns
6db4165c4c PM-19234 propagates attachment result errors to UI (#4869) 2025-03-14 20:47:25 +00:00
David Perez
18ce45e7e5 PM-19233: Propagate auth request errors to the UI (#4868) 2025-03-14 18:33:39 +00:00
Dave Severns
6fe9eba620 PM-11356 Adjust autofocus delay to be greater than screen refresh delay. (#4866) 2025-03-14 16:48:34 +00:00
David Perez
b084987758 PM-19226: Propagate error from create auth request flow to UI (#4867) 2025-03-14 15:47:03 +00:00
bw-ghapp[bot]
5d0593026f Autosync Crowdin Translations (#4865)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-14 14:39:07 +00:00
David Perez
47abeb7843 PM-14435: Improve accessibility service detection (#4864) 2025-03-14 14:16:25 +00:00
Phil Cappelli
e2779e4edb PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4863) 2025-03-13 20:46:14 +00:00
Álison Fernandes
90cc9f77c5 [PM-19207] Add Passkey / FIDO2 Bug Report template (#4859) 2025-03-13 20:31:30 +00:00
David Perez
40760f270a Use immutable map in debug menu (#4861) 2025-03-13 19:28:27 +00:00
David Perez
8a773141a4 Simplify RootNavScreenTest (#4860) 2025-03-13 19:20:14 +00:00
David Perez
0c149abdd9 PM-18844: Update BitwardenBasicDialog to allow it to share error logs (#4855) 2025-03-13 18:16:22 +00:00
David Perez
f540f86b19 PM-19199: hoist debug menu up to top level of the app (#4857) 2025-03-13 17:51:22 +00:00
Dave Severns
ca64ce2176 PM-18877 Respect system app specific language selection on Android 13 and up. (#4849) 2025-03-13 13:14:41 +00:00
André Bispo
da63c9e36b [PM-17995] Adjust custom fields section (#4835) 2025-03-13 11:33:39 +00:00
André Bispo
e16ad44d5e [PM-17242] While on autofill search on all item types. (#4824) 2025-03-13 11:33:12 +00:00
Phil Cappelli
d26a2ee52a PM-18681 - Update Showing Coach Mark Tour Logic To Account for Org Only Policy (#4854) 2025-03-12 20:20:44 +00:00
Dave Severns
1eb741ab58 PM-11356 prevent extra soft-keyboard showing. (#4845) 2025-03-11 20:10:46 +00:00
David Perez
e10ca9a6ec PM-19099: Centralize app metadata (#4847) 2025-03-11 19:47:07 +00:00
David Perez
3fca61ad3e Remove the language change dialog (#4658) 2025-03-11 14:33:22 +00:00
David Perez
409529b9ca Update AndroidX Activity to 1.10.1 (#4844) 2025-03-11 13:51:39 +00:00
David Perez
4568dd53d4 Update Firebase BOM to 33.10.0 (#4843) 2025-03-10 21:20:56 +00:00
David Perez
b9b90165bf PM-10725: Always show share sheet after creating send regardless of how it was made (#4841) 2025-03-10 20:43:49 +00:00
David Perez
778a630012 Update to AGP 8.9.0 (#4840) 2025-03-10 15:46:30 +00:00
Dave Severns
4809066ad7 PM-17087 update notification payloads to support camelCase JSON keys. (#4823) 2025-03-10 14:54:58 +00:00
Patrick Honkonen
d03c6c243d [PM-18873] Refactor ItemHeader.kt to improve location display (#4814) 2025-03-07 17:02:15 +00:00
bw-ghapp[bot]
d19ab498ff Autosync Crowdin Translations (#4832)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-03-07 16:48:31 +00:00
Phil Cappelli
efc3d21fde PM-18681 - Update Showing Coach Mark Tour Logic To Only Consider User's Personal Vault (#4821) 2025-03-05 16:01:41 +00:00
Phil Cappelli
35e585a60e PM-18570 Update Owner Selection Field to Bottom Sheet Selector (#4810) 2025-03-04 22:24:31 +00:00
Patrick Honkonen
39787f9bf0 [deps] Update mockk to 1.13.17 (#4818) 2025-03-04 20:17:42 +00:00
Patrick Honkonen
3940997ef9 [deps] Update junit5 to 5.11.4 (#4819) 2025-03-04 20:17:23 +00:00
Patrick Honkonen
ce482e744d [deps] Update testng to 7.11.0 (#4820) 2025-03-04 19:36:18 +00:00
Patrick Honkonen
b0157d10e2 [deps] Update detekt to 1.23.8 (#4817) 2025-03-04 19:36:00 +00:00
renovate[bot]
cf3c2fb56d [deps]: migrate renovate config (#4815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 17:56:09 +00:00
Dave Severns
a88a173e00 PM-18773 update the keyName for the ChromeAutofill flag (#4812) 2025-03-03 15:31:02 +00:00
aj-rosado
ac6ff98041 [PM-14435] Accessibility enabled settings changes to address older and custom Android phone versions (#4756) 2025-02-28 22:25:11 +00:00
David Perez
ec030f2c2e Update AGP to 8.8.2 (#4809) 2025-02-28 18:39:52 +00:00
Patrick Honkonen
be08c1a536 Refactor .editorconfig to focus on Kotlin and common file types (#4808) 2025-02-28 16:16:28 +00:00
bw-ghapp[bot]
84edf4ead0 Autosync Crowdin Translations (#4806)
Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com>
2025-02-28 14:34:04 +00:00
aj-rosado
915aac561c [PM-17215] Remove get_login_creds from Fido2OriginManagerImpl.filterMatchingAppStatementsOrNull (#4804) 2025-02-28 10:46:04 +00:00
Patrick Honkonen
2027a66f02 [PM-18714] Display Card brand icon when it is known (#4805) 2025-02-27 21:57:28 +00:00
David Perez
ef6d9bc68c PM-18677: Policies for disabled organizations apply (#4801) 2025-02-27 21:54:47 +00:00
Dave Severns
3ceda9e40a PM-18388 add hyphens on segmented button labels (#4777) 2025-02-27 21:50:37 +00:00
Phil Cappelli
3f1a6e97fd PM-17568 - Authenticator Sync: Sometimes synced verification codes only display the TOTP Key, not Issuer/username (#4783) 2025-02-27 21:42:38 +00:00
André Bispo
4448ab05ce [PM-17739] Show password history (#4803) 2025-02-27 19:49:57 +00:00
aj-rosado
d584391843 [PM-8223] 🍒 New device verification continue button enabled at 8 digit (#4802) 2025-02-27 19:48:24 +00:00
Patrick Honkonen
e0d91d7682 [PM-18067] Consolidate item name fields into ItemHeader (#4766) 2025-02-27 17:31:39 +00:00
Patrick Honkonen
e8a98dd3ed Add spacers to VaultItemIdentityContent, VaultItemSecureNoteContent, and VaultItemCardContent 2025-02-27 12:05:09 -05:00
Patrick Honkonen
b6163cf53c Fix item key in VaultItemCardContent for security code field 2025-02-27 11:14:40 -05:00
Patrick Honkonen
a24c6f2719 Update ic_organization.xml vector drawable 2025-02-27 11:13:04 -05:00
Patrick Honkonen
b3219d4040 Use CardStyle.Bottom when last item is drawn 2025-02-27 11:05:06 -05:00
Patrick Honkonen
27043e28a8 Revert formatting changes 2025-02-27 10:30:41 -05:00
Patrick Honkonen
7c36b7cb82 [PM-14303] Update Bitwarden SDK and load bitwarden_uniffi on older Android versions (#4793) 2025-02-27 15:23:55 +00:00
Patrick Honkonen
fcfcf48cee Uncomment & update previews 2025-02-27 09:17:50 -05:00
Patrick Honkonen
757994ec18 Merge remote-tracking branch 'origin/main' into PM-18067/view-item-favicon 2025-02-27 09:17:16 -05:00
Patrick Honkonen
71cd917328 Merge branch 'main' into PM-14303/update-bitwarden-sdk 2025-02-26 17:15:35 -05:00
Patrick Honkonen
5c076871ab Implement NativeLibraryManager and NativeLibraryManagerImpl for loading native libraries
This commit introduces the `NativeLibraryManager` interface and its implementation, `NativeLibraryManagerImpl`.
- `NativeLibraryManager` defines a method `loadLibrary` for loading native libraries.
- `NativeLibraryManagerImpl` uses `System.loadLibrary` to load libraries and handles potential `UnsatisfiedLinkError`.
- `PlatformManagerModule` is updated to provide `NativeLibraryManager` instance and to inject it into `SdkClientManager`.
2025-02-26 17:14:51 -05:00
Patrick Honkonen
07d3849c4b Add keys and animation to all content items 2025-02-26 17:10:10 -05:00
David Perez
33c3fd28e9 itemHeader as LazyListScope extension 2025-02-26 14:45:25 -06:00
Patrick Honkonen
88609c2f5b Update OrganizationType.OWNER mock data in VaultItemViewModelTest.kt 2025-02-26 14:45:25 -06:00
Patrick Honkonen
af8cfcd2f0 Move expansion indicator outside of crossfade animation 2025-02-26 14:45:25 -06:00
Patrick Honkonen
7cc8108498 Adjust ItemHeader expanding header fill the available width 2025-02-26 14:45:25 -06:00
Patrick Honkonen
a31c499b15 Remove unnecessary mockkStatic call in CipherViewExtensionsTest.kt 2025-02-26 14:45:24 -06:00
Patrick Honkonen
d7d099477f Refactor ItemHeader to use LazyColumn and Crossfade for smoother transitions
- Migrates `ItemHeader` to `LazyColumn` to improve performance.
- Introduces `Crossfade` for animating title changes in `BitwardenExpandingHeader`.
- Adjusts icon sizing in `ItemHeaderIcon`.
- Removes unnecessary column scope and animated visibility from `ExpandingItemLocationContent`.
- Refactors to use `LazyItemScope` and adds `animateItem()` to `ItemLocationListItem`.
- Adds conditional handling for expanding the item locations list.
2025-02-26 14:45:24 -06:00
Patrick Honkonen
0ba240852f Refactor ExpandingItemLocationContent to use ColumnScope 2025-02-26 14:45:24 -06:00
Patrick Honkonen
727d943fae Use persistentListOfNotNull instead of buildList and toImmutableList 2025-02-26 14:45:24 -06:00
Patrick Honkonen
234f49a92c Replace HorizontalDivider with BitwardenHorizontalDivider and add Spacer in ItemHeader.kt 2025-02-26 14:45:24 -06:00
Patrick Honkonen
4f49d3d504 Reduce Spacer height in VaultItemLoginContent.kt from 24.dp to 12.dp 2025-02-26 14:45:24 -06:00
Patrick Honkonen
aba8344df1 Revert unintentional change 2025-02-26 14:45:23 -06:00
Patrick Honkonen
6da8e2c47b Adjust height of Spacer in BitwardenTextField based on cardStyle presence 2025-02-26 14:45:23 -06:00
Patrick Honkonen
78d5965271 Refactor ItemHeader to use LazyColumn for overflow locations 2025-02-26 14:45:23 -06:00
Patrick Honkonen
6953d5e132 Migrate VaultItem related locations to ImmutableList 2025-02-26 14:45:23 -06:00
Patrick Honkonen
7073124495 Make cardStyle parameter optional in BitwardenTextField 2025-02-26 14:45:23 -06:00
Patrick Honkonen
0cc7067808 Add divider to ItemHeader in vault item view 2025-02-26 14:45:23 -06:00
Patrick Honkonen
9e920f1cf5 Refactor ItemHeader to use cardStyle and remove custom card styling. 2025-02-26 14:45:22 -06:00
Patrick Honkonen
537e743891 Replaced standardHorizontalMargin with explicit horizontal padding 2025-02-26 14:45:22 -06:00
Patrick Honkonen
60da236f3e Add illustration colors 2025-02-26 14:45:22 -06:00
Patrick Honkonen
7804d8430f [PM-18067] Consolidate item name fields into ItemHeader
This commit introduces `ItemHeader`, a new composable that replaces `ItemNameField` to display the item name, favorite status, and related details like organization, collections, and folder.

Key changes:
- Removes `ItemNameField`
- Adds `ItemHeader` for displaying item name and favorite status, along with item location information.
- Introduces a new `ic_organization` icon.
- Adds the logic for showing item locations (organization, collections, folders) in a collapsible view.
- Removes `ItemNameField` from `VaultItemLoginContent`, `VaultItemIdentityContent`, `VaultItemSecureNoteContent`, `VaultItemCardContent`, `VaultItemSshKeyContent` and replace it with `ItemHeader`
- Adds the logic to fetch and display the item icon in `ItemHeader` based on item type
- Adds an `ItemLocationListItem` for displaying location details.
- Adds a `VaultItemLocation` data class for representing item locations.
- Adds new `baseIconUrl` and `isIconLoadingDisabled` variables to the `VaultItemState` to handle icon display.
- Updates `CipherView.toIconData` to handle the item icon.
- Adds new `show_more`, `no_folder` and `show_less` string resources.
- Updates the `BitwardenShapes` to include `favicon` shapes.
- Updates the `BitwardenColorScheme` to include `faviconForeground` and `faviconBackground`.
- Updates `BitwardenExpandingHeader` to include expandedText, collapsedText and showExpansionIndicator properties.
2025-02-26 14:45:22 -06:00
Patrick Honkonen
dab06b0ed4 [PM-14303] Update Bitwarden SDK and load bitwarden_uniffi on older Android versions
The Bitwarden SDK dependency was updated to version 1.0.0-20250225.125021-120.

The SDK requires access to Android APIs that were made public in API 31 in order to generate email aliases. To address this limitation for devices with earlier API versions, the `bitwarden_uniffi` library is now loaded manually before initializing any `Client` instance.
2025-02-26 10:04:24 -05:00
392 changed files with 9415 additions and 3534 deletions

View File

@@ -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

64
.github/ISSUE_TEMPLATE/bug-passkey.yml vendored Normal file
View 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.

51
.github/renovate.json vendored
View File

@@ -1,33 +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",
"androidx.lifecycle:*",
"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"
]
}
]
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -99,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
@@ -467,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(
@@ -501,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 -> {
@@ -524,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)
@@ -577,7 +576,7 @@ class AuthRepositoryImpl(
}
.fold(
onSuccess = { NewSsoUserResult.Success },
onFailure = { NewSsoUserResult.Failure },
onFailure = { NewSsoUserResult.Failure(error = it) },
)
}
@@ -586,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 ->
@@ -639,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 },
@@ -688,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,
@@ -708,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,
@@ -767,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 },
)
@@ -777,7 +785,7 @@ class AuthRepositoryImpl(
passcode = oneTimePasscode,
)
.fold(
onFailure = { VerifyOtpResult.NotVerified(it.message) },
onFailure = { VerifyOtpResult.NotVerified(errorMessage = it.message, error = it) },
onSuccess = { VerifyOtpResult.Verified },
)
@@ -785,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
@@ -901,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 -> {
@@ -910,11 +927,11 @@ class AuthRepositoryImpl(
}
is RegisterResponseJson.Invalid -> {
RegisterResult.Error(errorMessage = it.message)
RegisterResult.Error(errorMessage = it.message, error = null)
}
}
},
onFailure = { RegisterResult.Error(errorMessage = null) },
onFailure = { RegisterResult.Error(errorMessage = null, error = it) },
)
}
@@ -922,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) },
)
}
@@ -934,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 &&
@@ -947,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,
@@ -965,7 +988,7 @@ class AuthRepositoryImpl(
settingsRepository.setDefaultsIfNecessary(userId = userId)
}
.fold(
onFailure = { RemovePasswordResult.Error },
onFailure = { RemovePasswordResult.Error(error = it) },
onSuccess = { RemovePasswordResult.Success },
)
}
@@ -978,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(
@@ -988,7 +1011,7 @@ class AuthRepositoryImpl(
purpose = HashPurpose.SERVER_AUTHORIZATION,
)
.fold(
onFailure = { return ResetPasswordResult.Error },
onFailure = { return ResetPasswordResult.Error(error = it) },
onSuccess = { it },
)
}
@@ -1034,7 +1057,7 @@ class AuthRepositoryImpl(
// Return the success.
ResetPasswordResult.Success
},
onFailure = { ResetPasswordResult.Error },
onFailure = { ResetPasswordResult.Error(error = it) },
)
}
@@ -1047,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.
@@ -1058,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 -> {
@@ -1108,7 +1131,7 @@ class AuthRepositoryImpl(
}
}
.flatMap {
when (vaultRepository.unlockVaultWithMasterPassword(password)) {
when (val result = vaultRepository.unlockVaultWithMasterPassword(password)) {
is VaultUnlockResult.Success -> {
enrollUserInPasswordReset(
userId = userId,
@@ -1117,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()
}
}
}
@@ -1132,7 +1152,7 @@ class AuthRepositoryImpl(
this.organizationIdentifier = null
}
.fold(
onFailure = { SetPasswordResult.Error },
onFailure = { SetPasswordResult.Error(error = it) },
onSuccess = { SetPasswordResult.Success },
)
}
@@ -1167,7 +1187,7 @@ class AuthRepositoryImpl(
verifiedDate = it.verifiedDate,
)
},
onFailure = { OrganizationDomainSsoDetailsResult.Failure },
onFailure = { OrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun getVerifiedOrganizationDomainSsoDetails(
@@ -1182,7 +1202,7 @@ class AuthRepositoryImpl(
verifiedOrganizationDomainSsoDetails = it.verifiedOrganizationDomainSsoDetails,
)
},
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure },
onFailure = { VerifiedOrganizationDomainSsoDetailsResult.Failure(error = it) },
)
override suspend fun prevalidateSso(
@@ -1195,19 +1215,19 @@ class AuthRepositoryImpl(
onSuccess = {
when (it) {
is PrevalidateSsoResponseJson.Error -> {
PrevalidateSsoResult.Failure(message = it.message)
PrevalidateSsoResult.Failure(message = it.message, error = null)
}
is PrevalidateSsoResponseJson.Success -> {
if (it.token.isNullOrBlank()) {
PrevalidateSsoResult.Failure()
PrevalidateSsoResult.Failure(error = MissingPropertyException("Token"))
} else {
PrevalidateSsoResult.Success(token = it.token)
}
}
}
},
onFailure = { PrevalidateSsoResult.Failure() },
onFailure = { PrevalidateSsoResult.Failure(error = it) },
)
override fun setSsoCallbackResult(result: SsoCallbackResult) {
@@ -1221,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) },
)
@@ -1249,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 ->
@@ -1265,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,
@@ -1301,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,
@@ -1313,7 +1335,7 @@ class AuthRepositoryImpl(
)
.fold(
onSuccess = { ValidatePinResult.Success(isValid = it) },
onFailure = { ValidatePinResult.Error },
onFailure = { ValidatePinResult.Error(error = it) },
)
}
@@ -1339,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 -> {
@@ -1347,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 {
@@ -1365,15 +1388,13 @@ 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) },
)
}
@@ -1645,7 +1666,10 @@ class AuthRepositoryImpl(
LoginResult.UnofficialServerError
}
else -> LoginResult.Error(errorMessage = null)
else -> LoginResult.Error(
errorMessage = null,
error = throwable,
)
}
},
onSuccess = { loginResponse ->
@@ -1681,6 +1705,7 @@ class AuthRepositoryImpl(
is GetTokenResponseJson.Invalid.InvalidType.GenericInvalid -> {
LoginResult.Error(
errorMessage = loginResponse.errorMessage,
error = null,
)
}
}
@@ -1883,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 {
@@ -1923,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 },
)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -22,5 +22,7 @@ sealed class OrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : OrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : OrganizationDomainSsoDetailsResult()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -16,5 +16,6 @@ sealed class PrevalidateSsoResult {
*/
data class Failure(
val message: String? = null,
val error: Throwable?,
) : PrevalidateSsoResult()
}

View File

@@ -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.

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -18,5 +18,7 @@ sealed class VerifiedOrganizationDomainSsoDetailsResult {
/**
* The request failed.
*/
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
data class Failure(
val error: Throwable,
) : VerifiedOrganizationDomainSsoDetailsResult()
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -57,10 +57,10 @@ object AccessibilityModule {
@Singleton
@Provides
fun providesAccessibilityEnabledManager(
accessibilityManager: AccessibilityManager,
@ApplicationContext context: Context,
): AccessibilityEnabledManager =
AccessibilityEnabledManagerImpl(
accessibilityManager = accessibilityManager,
context = context,
)
@Singleton

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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",
),
)

View File

@@ -267,7 +267,7 @@ class Fido2ProviderProcessorImpl(
val result = vaultRepository
.getDecryptedFido2CredentialAutofillViews(cipherViews)
return when (result) {
DecryptFido2CredentialAutofillViewResult.Error -> {
is DecryptFido2CredentialAutofillViewResult.Error -> {
throw GetCredentialUnknownException("Error decrypting credentials.")
}

View File

@@ -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")

View File

@@ -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!")

View File

@@ -158,7 +158,7 @@ class FirstTimeActionManagerImpl @Inject constructor(
override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowAddLoginCoachMarkFlow()
.map { it ?: true }
.map { it != false }
.mapFalseIfAnyLoginCiphersAvailable()
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
@@ -172,11 +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()
@@ -298,8 +300,8 @@ class FirstTimeActionManagerImpl @Inject constructor(
}
/**
* If there are any existing "Login" type ciphers then we'll map the current value
* of the receiver Flow to `false`.
* 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> =
@@ -310,8 +312,10 @@ class FirstTimeActionManagerImpl @Inject constructor(
combine(
flow = this,
flow2 = vaultDiskSource.getCiphers(activeUserId),
) { currentValue, ciphers ->
currentValue && ciphers.none { it.login != null }
) { receiverCurrentValue, ciphers ->
receiverCurrentValue && ciphers.none {
it.login != null && it.organizationId == null
}
}
}
.distinctUntilChanged()

View File

@@ -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)

View File

@@ -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>
}

View File

@@ -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)
}
}
}

View File

@@ -70,7 +70,6 @@ class PolicyManagerImpl(
.getOrganizations(userId)
?.filter {
it.shouldUsePolicies &&
it.isEnabled &&
it.status >= OrganizationStatusType.ACCEPTED &&
!isOrganizationExemptFromPolicies(it, type)
}

View File

@@ -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() }

View File

@@ -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

View File

@@ -44,12 +44,13 @@ sealed class FlagKey<out T : Any> {
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.
*/
@@ -218,7 +228,7 @@ sealed class FlagKey<out T : Any> {
* autofill.
*/
data object ChromeAutofill : FlagKey<Boolean>() {
override val keyName: String = "enable-pm-chrome-autofill"
override val keyName: String = "android-chrome-autofill"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = true
}

View File

@@ -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()
}
}

View File

@@ -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()
}

View File

@@ -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:

View File

@@ -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) },
)
}

View File

@@ -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()
}

View File

@@ -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"
}
}

View File

@@ -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()

View File

@@ -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)
},
)
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -12,5 +12,5 @@ sealed class GeneratedPassphraseResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPassphraseResult()
data class InvalidRequest(val error: Throwable) : GeneratedPassphraseResult()
}

View File

@@ -13,5 +13,5 @@ sealed class GeneratedPasswordResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPasswordResult()
data class InvalidRequest(val error: Throwable) : GeneratedPasswordResult()
}

View File

@@ -14,5 +14,5 @@ sealed class GeneratedPlusAddressedUsernameResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedPlusAddressedUsernameResult()
data class InvalidRequest(val error: Throwable) : GeneratedPlusAddressedUsernameResult()
}

View File

@@ -15,5 +15,5 @@ sealed class GeneratedRandomWordUsernameResult {
/**
* There was an error during the operation.
*/
data object InvalidRequest : GeneratedRandomWordUsernameResult()
data class InvalidRequest(val error: Throwable) : GeneratedRandomWordUsernameResult()
}

View File

@@ -169,7 +169,10 @@ class VaultSdkSourceImpl(
InitializeCryptoResult.Success
} catch (exception: BitwardenException) {
// The only truly expected error from the SDK is an incorrect key/password.
InitializeCryptoResult.AuthenticationError(message = exception.message)
InitializeCryptoResult.AuthenticationError(
message = exception.message,
error = exception,
)
}
}
@@ -187,6 +190,7 @@ class VaultSdkSourceImpl(
// The only truly expected error from the SDK is for incorrect keys.
InitializeCryptoResult.AuthenticationError(
message = exception.message,
error = exception,
)
}
}

View File

@@ -1,19 +0,0 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
import com.bitwarden.vault.CipherView
/**
* Models the result of querying for ciphers with FIDO 2 credentials.
*/
sealed class FindFido2CredentialsResult {
/**
* Indicates the query was executed successfully.
*/
data class Success(val cipherViews: List<CipherView>) : FindFido2CredentialsResult()
/**
* Indicates the query was not executed successfully.
*/
data object Error : FindFido2CredentialsResult()
}

View File

@@ -15,5 +15,6 @@ sealed class InitializeCryptoResult {
*/
data class AuthenticationError(
val message: String? = null,
val error: Throwable,
) : InitializeCryptoResult()
}

View File

@@ -1,17 +0,0 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk.model
/**
* Models the result of saving a FIDO 2 credential.
*/
sealed class SaveCredentialResult {
/**
* Indicates the credential has been saved.
*/
data object Success : SaveCredentialResult()
/**
* Indicates the credential was not saved.
*/
data object Error : SaveCredentialResult()
}

View File

@@ -6,6 +6,7 @@ import com.bitwarden.vault.AttachmentView
import com.bitwarden.vault.Cipher
import com.bitwarden.vault.CipherView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
import com.x8bit.bitwarden.data.platform.util.asFailure
import com.x8bit.bitwarden.data.platform.util.asSuccess
@@ -49,7 +50,8 @@ class CipherManagerImpl(
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
override suspend fun createCipher(cipherView: CipherView): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -58,7 +60,7 @@ class CipherManagerImpl(
.flatMap { ciphersService.createCipher(body = it.toEncryptedNetworkCipher()) }
.onSuccess { vaultDiskSource.saveCipher(userId = userId, cipher = it) }
.fold(
onFailure = { CreateCipherResult.Error },
onFailure = { CreateCipherResult.Error(error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
@@ -70,7 +72,8 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): CreateCipherResult {
val userId = activeUserId ?: return CreateCipherResult.Error
val userId = activeUserId
?: return CreateCipherResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -91,7 +94,7 @@ class CipherManagerImpl(
)
}
.fold(
onFailure = { CreateCipherResult.Error },
onFailure = { CreateCipherResult.Error(error = it) },
onSuccess = {
reviewPromptManager.registerAddCipherAction()
CreateCipherResult.Success
@@ -100,13 +103,14 @@ class CipherManagerImpl(
}
override suspend fun hardDeleteCipher(cipherId: String): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
val userId = activeUserId
?: return DeleteCipherResult.Error(error = NoActiveUserException())
return ciphersService
.hardDeleteCipher(cipherId = cipherId)
.onSuccess { vaultDiskSource.deleteCipher(userId = userId, cipherId = cipherId) }
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
onFailure = { DeleteCipherResult.Error(error = it) },
)
}
@@ -114,7 +118,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): DeleteCipherResult {
val userId = activeUserId ?: return DeleteCipherResult.Error
val userId = activeUserId
?: return DeleteCipherResult.Error(error = NoActiveUserException())
return cipherView
.encryptCipherAndCheckForMigration(userId = userId, cipherId = cipherId)
.flatMap { cipher ->
@@ -136,7 +141,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { DeleteCipherResult.Success },
onFailure = { DeleteCipherResult.Error },
onFailure = { DeleteCipherResult.Error(error = it) },
)
}
@@ -152,7 +157,7 @@ class CipherManagerImpl(
)
.fold(
onSuccess = { DeleteAttachmentResult.Success },
onFailure = { DeleteAttachmentResult.Error },
onFailure = { DeleteAttachmentResult.Error(error = it) },
)
private suspend fun deleteCipherAttachmentForResult(
@@ -160,7 +165,7 @@ class CipherManagerImpl(
attachmentId: String,
cipherView: CipherView,
): Result<Cipher> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
return ciphersService
.deleteCipherAttachment(
cipherId = cipherId,
@@ -187,7 +192,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): RestoreCipherResult {
val userId = activeUserId ?: return RestoreCipherResult.Error
val userId = activeUserId
?: return RestoreCipherResult.Error(error = NoActiveUserException())
return ciphersService
.restoreCipher(cipherId = cipherId)
.onSuccess {
@@ -198,7 +204,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { RestoreCipherResult.Success },
onFailure = { RestoreCipherResult.Error },
onFailure = { RestoreCipherResult.Error(error = it) },
)
}
@@ -206,7 +212,8 @@ class CipherManagerImpl(
cipherId: String,
cipherView: CipherView,
): UpdateCipherResult {
val userId = activeUserId ?: return UpdateCipherResult.Error(errorMessage = null)
val userId = activeUserId
?: return UpdateCipherResult.Error(errorMessage = null, error = NoActiveUserException())
return vaultSdkSource
.encryptCipher(
userId = userId,
@@ -221,7 +228,7 @@ class CipherManagerImpl(
.map { response ->
when (response) {
is UpdateCipherResponseJson.Invalid -> {
UpdateCipherResult.Error(errorMessage = response.message)
UpdateCipherResult.Error(errorMessage = response.message, error = null)
}
is UpdateCipherResponseJson.Success -> {
@@ -234,7 +241,7 @@ class CipherManagerImpl(
}
}
.fold(
onFailure = { UpdateCipherResult.Error(errorMessage = null) },
onFailure = { UpdateCipherResult.Error(errorMessage = null, error = it) },
onSuccess = { it },
)
}
@@ -245,7 +252,7 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
val userId = activeUserId ?: return ShareCipherResult.Error(error = NoActiveUserException())
return migrateAttachments(userId = userId, cipherView = cipherView)
.flatMap {
vaultSdkSource.moveToOrganization(
@@ -271,7 +278,7 @@ class CipherManagerImpl(
)
}
.fold(
onFailure = { ShareCipherResult.Error },
onFailure = { ShareCipherResult.Error(error = it) },
onSuccess = { ShareCipherResult.Success },
)
}
@@ -281,7 +288,7 @@ class CipherManagerImpl(
cipherView: CipherView,
collectionIds: List<String>,
): ShareCipherResult {
val userId = activeUserId ?: return ShareCipherResult.Error
val userId = activeUserId ?: return ShareCipherResult.Error(error = NoActiveUserException())
return ciphersService
.updateCipherCollections(
cipherId = cipherId,
@@ -301,7 +308,7 @@ class CipherManagerImpl(
}
.fold(
onSuccess = { ShareCipherResult.Success },
onFailure = { ShareCipherResult.Error },
onFailure = { ShareCipherResult.Error(error = it) },
)
}
@@ -320,7 +327,7 @@ class CipherManagerImpl(
fileUri = fileUri,
)
.fold(
onFailure = { CreateAttachmentResult.Error },
onFailure = { CreateAttachmentResult.Error(error = it) },
onSuccess = { CreateAttachmentResult.Success(cipherView = it) },
)
@@ -332,7 +339,7 @@ class CipherManagerImpl(
fileName: String?,
fileUri: Uri,
): Result<CipherView> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
val attachmentView = AttachmentView(
id = null,
url = null,
@@ -404,14 +411,14 @@ class CipherManagerImpl(
)
.fold(
onSuccess = { DownloadAttachmentResult.Success(file = it) },
onFailure = { DownloadAttachmentResult.Failure },
onFailure = { DownloadAttachmentResult.Failure(error = it) },
)
private suspend fun downloadAttachmentForResult(
cipherView: CipherView,
attachmentId: String,
): Result<File> {
val userId = activeUserId ?: return IllegalStateException("No active user").asFailure()
val userId = activeUserId ?: return NoActiveUserException().asFailure()
val cipher = cipherView
.encryptCipherAndCheckForMigration(
@@ -439,7 +446,10 @@ class CipherManagerImpl(
?: return IllegalStateException("Attachment does not have a url").asFailure()
val encryptedFile = when (val result = fileManager.downloadFileToCache(url)) {
DownloadResult.Failure -> return IllegalStateException("Download failed").asFailure()
is DownloadResult.Failure -> {
return IllegalStateException("Download failed", result.error).asFailure()
}
is DownloadResult.Success -> result.file
}

View File

@@ -44,7 +44,7 @@ class FileManagerImpl(
.getDataStream(url)
.fold(
onSuccess = { it },
onFailure = { return DownloadResult.Failure },
onFailure = { return DownloadResult.Failure(error = it) },
)
// Create a temporary file in cache to write to
@@ -66,7 +66,7 @@ class FileManagerImpl(
}
fos.flush()
} catch (e: RuntimeException) {
return@withContext DownloadResult.Failure
return@withContext DownloadResult.Failure(error = e)
}
}
}
@@ -94,7 +94,7 @@ class FileManagerImpl(
}
}
true
} catch (exception: RuntimeException) {
} catch (_: RuntimeException) {
false
}
}
@@ -111,7 +111,7 @@ class FileManagerImpl(
}
}
true
} catch (exception: RuntimeException) {
} catch (_: RuntimeException) {
false
}
}

View File

@@ -16,6 +16,8 @@ import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.userAccountTokens
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.AppCreationState
@@ -167,7 +169,7 @@ class VaultLockManagerImpl(
.fold(
onFailure = {
incrementInvalidUnlockCount(userId = userId)
VaultUnlockResult.GenericError
VaultUnlockResult.GenericError(error = it)
},
onSuccess = { initializeCryptoResult ->
initializeCryptoResult
@@ -605,9 +607,11 @@ class VaultLockManagerImpl(
initUserCryptoMethod: InitUserCryptoMethod,
): VaultUnlockResult {
val account = authDiskSource.userState?.accounts?.get(userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Private key"),
)
val organizationKeys = authDiskSource.getOrganizationKeys(userId = userId)
return unlockVault(
userId = userId,

View File

@@ -14,5 +14,7 @@ sealed class DownloadResult {
/**
* The download failed.
*/
data object Failure : DownloadResult()
data class Failure(
val error: Throwable,
) : DownloadResult()
}

View File

@@ -21,6 +21,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.ReviewPromptManager
@@ -509,11 +511,13 @@ class VaultRepositoryImpl(
): DecryptFido2CredentialAutofillViewResult {
return vaultSdkSource
.decryptFido2CredentialAutofillViews(
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error,
userId = activeUserId ?: return DecryptFido2CredentialAutofillViewResult.Error(
error = NoActiveUserException(),
),
cipherViews = cipherViewList.toTypedArray(),
)
.fold(
onFailure = { DecryptFido2CredentialAutofillViewResult.Error },
onFailure = { DecryptFido2CredentialAutofillViewResult.Error(error = it) },
onSuccess = { DecryptFido2CredentialAutofillViewResult.Success(it) },
)
}
@@ -545,10 +549,13 @@ class VaultRepositoryImpl(
)
override suspend fun unlockVaultWithBiometrics(cipher: Cipher): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val biometricsKey = authDiskSource
.getUserBiometricUnlockKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Biometric key"),
)
val iv = authDiskSource.getUserBiometricInitVector(userId = userId)
return this
.unlockVaultForUser(
@@ -560,8 +567,8 @@ class VaultRepositoryImpl(
cipher
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
.decodeToString()
} catch (_: GeneralSecurityException) {
return VaultUnlockResult.BiometricDecodingError
} catch (e: GeneralSecurityException) {
return VaultUnlockResult.BiometricDecodingError(error = e)
}
}
?: biometricsKey,
@@ -586,9 +593,12 @@ class VaultRepositoryImpl(
override suspend fun unlockVaultWithMasterPassword(
masterPassword: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val userKey = authDiskSource.getUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("User key"),
)
return unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Password(
@@ -606,9 +616,12 @@ class VaultRepositoryImpl(
override suspend fun unlockVaultWithPin(
pin: String,
): VaultUnlockResult {
val userId = activeUserId ?: return VaultUnlockResult.InvalidStateError
val userId = activeUserId
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val pinProtectedUserKey = authDiskSource.getPinProtectedUserKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Pin protected key"),
)
return unlockVaultForUser(
userId = userId,
initUserCryptoMethod = InitUserCryptoMethod.Pin(
@@ -622,7 +635,8 @@ class VaultRepositoryImpl(
sendView: SendView,
fileUri: Uri?,
): CreateSendResult {
val userId = activeUserId ?: return CreateSendResult.Error(message = null)
val userId = activeUserId
?: return CreateSendResult.Error(message = null, error = NoActiveUserException())
return vaultSdkSource
.encryptSend(
userId = userId,
@@ -639,6 +653,7 @@ class VaultRepositoryImpl(
is CreateSendJsonResponse.Invalid -> {
return CreateSendResult.Error(
message = createSendResponse.firstValidationErrorMessage,
error = null,
)
}
@@ -656,7 +671,7 @@ class VaultRepositoryImpl(
)
}
.fold(
onFailure = { CreateSendResult.Error(message = null) },
onFailure = { CreateSendResult.Error(message = null, error = it) },
onSuccess = {
reviewPromptManager.registerCreateSendAction()
CreateSendResult.Success(it)
@@ -668,7 +683,11 @@ class VaultRepositoryImpl(
sendId: String,
sendView: SendView,
): UpdateSendResult {
val userId = activeUserId ?: return UpdateSendResult.Error(null)
val userId = activeUserId
?: return UpdateSendResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptSend(
userId = userId,
@@ -681,11 +700,11 @@ class VaultRepositoryImpl(
)
}
.fold(
onFailure = { UpdateSendResult.Error(errorMessage = null) },
onFailure = { UpdateSendResult.Error(errorMessage = null, error = it) },
onSuccess = { response ->
when (response) {
is UpdateSendResponseJson.Invalid -> {
UpdateSendResult.Error(errorMessage = response.message)
UpdateSendResult.Error(errorMessage = response.message, error = null)
}
is UpdateSendResponseJson.Success -> {
@@ -695,9 +714,12 @@ class VaultRepositoryImpl(
userId = userId,
send = response.send.toEncryptedSdkSend(),
)
.getOrNull()
?.let { UpdateSendResult.Success(sendView = it) }
?: UpdateSendResult.Error(errorMessage = null)
.fold(
onSuccess = { UpdateSendResult.Success(sendView = it) },
onFailure = {
UpdateSendResult.Error(errorMessage = null, error = it)
},
)
}
}
},
@@ -705,14 +727,21 @@ class VaultRepositoryImpl(
}
override suspend fun removePasswordSend(sendId: String): RemovePasswordSendResult {
val userId = activeUserId ?: return RemovePasswordSendResult.Error(null)
val userId = activeUserId
?: return RemovePasswordSendResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return sendsService
.removeSendPassword(sendId = sendId)
.fold(
onSuccess = { response ->
when (response) {
is UpdateSendResponseJson.Invalid -> {
RemovePasswordSendResult.Error(errorMessage = response.message)
RemovePasswordSendResult.Error(
errorMessage = response.message,
error = null,
)
}
is UpdateSendResponseJson.Success -> {
@@ -722,24 +751,30 @@ class VaultRepositoryImpl(
userId = userId,
send = response.send.toEncryptedSdkSend(),
)
.getOrNull()
?.let { RemovePasswordSendResult.Success(sendView = it) }
?: RemovePasswordSendResult.Error(errorMessage = null)
.fold(
onSuccess = { RemovePasswordSendResult.Success(sendView = it) },
onFailure = {
RemovePasswordSendResult.Error(
errorMessage = null,
error = it,
)
},
)
}
}
},
onFailure = { RemovePasswordSendResult.Error(errorMessage = null) },
onFailure = { RemovePasswordSendResult.Error(errorMessage = null, error = it) },
)
}
override suspend fun deleteSend(sendId: String): DeleteSendResult {
val userId = activeUserId ?: return DeleteSendResult.Error
val userId = activeUserId ?: return DeleteSendResult.Error(error = NoActiveUserException())
return sendsService
.deleteSend(sendId)
.onSuccess { vaultDiskSource.deleteSend(userId, sendId) }
.fold(
onSuccess = { DeleteSendResult.Success },
onFailure = { DeleteSendResult.Error },
onFailure = { DeleteSendResult.Error(error = it) },
)
}
@@ -747,7 +782,8 @@ class VaultRepositoryImpl(
totpCode: String,
time: DateTime,
): GenerateTotpResult {
val userId = activeUserId ?: return GenerateTotpResult.Error
val userId = activeUserId
?: return GenerateTotpResult.Error(error = NoActiveUserException())
return vaultSdkSource.generateTotp(
time = time,
userId = userId,
@@ -760,12 +796,13 @@ class VaultRepositoryImpl(
periodSeconds = it.period.toInt(),
)
},
onFailure = { GenerateTotpResult.Error },
onFailure = { GenerateTotpResult.Error(error = it) },
)
}
override suspend fun createFolder(folderView: FolderView): CreateFolderResult {
val userId = activeUserId ?: return CreateFolderResult.Error
val userId = activeUserId
?: return CreateFolderResult.Error(error = NoActiveUserException())
return vaultSdkSource
.encryptFolder(
userId = userId,
@@ -781,7 +818,7 @@ class VaultRepositoryImpl(
.flatMap { vaultSdkSource.decryptFolder(userId, it.toEncryptedSdkFolder()) }
.fold(
onSuccess = { CreateFolderResult.Success(folderView = it) },
onFailure = { CreateFolderResult.Error },
onFailure = { CreateFolderResult.Error(error = it) },
)
}
@@ -789,7 +826,10 @@ class VaultRepositoryImpl(
folderId: String,
folderView: FolderView,
): UpdateFolderResult {
val userId = activeUserId ?: return UpdateFolderResult.Error(null)
val userId = activeUserId ?: return UpdateFolderResult.Error(
errorMessage = null,
error = NoActiveUserException(),
)
return vaultSdkSource
.encryptFolder(
userId = userId,
@@ -814,21 +854,30 @@ class VaultRepositoryImpl(
)
.fold(
onSuccess = { UpdateFolderResult.Success(it) },
onFailure = { UpdateFolderResult.Error(errorMessage = null) },
onFailure = {
UpdateFolderResult.Error(
errorMessage = null,
error = it,
)
},
)
}
is UpdateFolderResponseJson.Invalid -> {
UpdateFolderResult.Error(response.message)
UpdateFolderResult.Error(
errorMessage = response.message,
error = null,
)
}
}
},
onFailure = { UpdateFolderResult.Error(it.message) },
onFailure = { UpdateFolderResult.Error(it.message, error = it) },
)
}
override suspend fun deleteFolder(folderId: String): DeleteFolderResult {
val userId = activeUserId ?: return DeleteFolderResult.Error
val userId = activeUserId
?: return DeleteFolderResult.Error(error = NoActiveUserException())
return folderService
.deleteFolder(
folderId = folderId,
@@ -839,7 +888,7 @@ class VaultRepositoryImpl(
}
.fold(
onSuccess = { DeleteFolderResult.Success },
onFailure = { DeleteFolderResult.Error },
onFailure = { DeleteFolderResult.Error(error = it) },
)
}
@@ -854,7 +903,8 @@ class VaultRepositoryImpl(
}
override suspend fun exportVaultDataToString(format: ExportFormat): ExportVaultDataResult {
val userId = activeUserId ?: return ExportVaultDataResult.Error
val userId = activeUserId
?: return ExportVaultDataResult.Error(error = NoActiveUserException())
val folders = vaultDiskSource
.getFolders(userId)
.firstOrNull()
@@ -877,7 +927,7 @@ class VaultRepositoryImpl(
)
.fold(
onSuccess = { ExportVaultDataResult.Success(it) },
onFailure = { ExportVaultDataResult.Error },
onFailure = { ExportVaultDataResult.Error(error = it) },
)
}
@@ -945,9 +995,11 @@ class VaultRepositoryImpl(
initUserCryptoMethod: InitUserCryptoMethod,
): VaultUnlockResult {
val account = authDiskSource.userState?.accounts?.get(userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(error = NoActiveUserException())
val privateKey = authDiskSource.getPrivateKey(userId = userId)
?: return VaultUnlockResult.InvalidStateError
?: return VaultUnlockResult.InvalidStateError(
error = MissingPropertyException("Private key"),
)
val organizationKeys = authDiskSource
.getOrganizationKeys(userId = userId)
return unlockVault(

View File

@@ -17,5 +17,5 @@ sealed class CreateAttachmentResult {
/**
* Generic error while creating an attachment.
*/
data object Error : CreateAttachmentResult()
data class Error(val error: Throwable) : CreateAttachmentResult()
}

View File

@@ -13,5 +13,5 @@ sealed class CreateCipherResult {
/**
* Generic error while creating cipher.
*/
data object Error : CreateCipherResult()
data class Error(val error: Throwable) : CreateCipherResult()
}

View File

@@ -15,5 +15,5 @@ sealed class CreateFolderResult {
/**
* Generic error while creating a folder.
*/
data object Error : CreateFolderResult()
data class Error(val error: Throwable) : CreateFolderResult()
}

View File

@@ -15,5 +15,5 @@ sealed class CreateSendResult {
/**
* Generic error while creating a send.
*/
data class Error(val message: String?) : CreateSendResult()
data class Error(val message: String?, val error: Throwable?) : CreateSendResult()
}

View File

@@ -16,5 +16,5 @@ sealed class DecryptFido2CredentialAutofillViewResult {
/**
* Generic error while decrypting credentials.
*/
data object Error : DecryptFido2CredentialAutofillViewResult()
data class Error(val error: Throwable) : DecryptFido2CredentialAutofillViewResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteAttachmentResult {
/**
* Generic error while deleting an attachment.
*/
data object Error : DeleteAttachmentResult()
data class Error(val error: Throwable) : DeleteAttachmentResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteCipherResult {
/**
* Generic error while deleting a cipher.
*/
data object Error : DeleteCipherResult()
data class Error(val error: Throwable) : DeleteCipherResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteFolderResult {
/**
* Generic error while deleting a folder.
*/
data object Error : DeleteFolderResult()
data class Error(val error: Throwable) : DeleteFolderResult()
}

View File

@@ -13,5 +13,5 @@ sealed class DeleteSendResult {
/**
* Generic error while deleting a send.
*/
data object Error : DeleteSendResult()
data class Error(val error: Throwable) : DeleteSendResult()
}

View File

@@ -14,5 +14,5 @@ sealed class DownloadAttachmentResult {
/**
* The attachment could not be downloaded.
*/
data object Failure : DownloadAttachmentResult()
data class Failure(val error: Throwable) : DownloadAttachmentResult()
}

View File

@@ -14,5 +14,5 @@ sealed class ExportVaultDataResult {
/**
* There was an error converting the vault data.
*/
data object Error : ExportVaultDataResult()
data class Error(val error: Throwable) : ExportVaultDataResult()
}

View File

@@ -16,5 +16,5 @@ sealed class GenerateTotpResult {
/**
* An error occurred while generating the code.
*/
data object Error : GenerateTotpResult()
data class Error(val error: Throwable) : GenerateTotpResult()
}

View File

@@ -17,5 +17,5 @@ sealed class RemovePasswordSendResult {
* Generic error while removing the password protection from a send. The optional
* [errorMessage] may be displayed directly in the UI when present.
*/
data class Error(val errorMessage: String?) : RemovePasswordSendResult()
data class Error(val errorMessage: String?, val error: Throwable?) : RemovePasswordSendResult()
}

View File

@@ -13,5 +13,5 @@ sealed class RestoreCipherResult {
/**
* Generic error while restoring a cipher.
*/
data object Error : RestoreCipherResult()
data class Error(val error: Throwable) : RestoreCipherResult()
}

View File

@@ -12,5 +12,5 @@ sealed class ShareCipherResult {
/**
* Generic error while sharing cipher.
*/
data object Error : ShareCipherResult()
data class Error(val error: Throwable) : ShareCipherResult()
}

View File

@@ -13,5 +13,5 @@ sealed class TotpCodeResult {
/**
* There was an error scanning the code.
*/
data object CodeScanningError : TotpCodeResult()
data class CodeScanningError(val error: Throwable? = null) : TotpCodeResult()
}

View File

@@ -14,5 +14,5 @@ sealed class UpdateCipherResult {
* Generic error while updating cipher. The optional [errorMessage] may be displayed directly in
* the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateCipherResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateCipherResult()
}

View File

@@ -16,5 +16,5 @@ sealed class UpdateFolderResult {
* Generic error while updating a folder. The optional [errorMessage]
* may be displayed directly in the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateFolderResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateFolderResult()
}

View File

@@ -16,5 +16,5 @@ sealed class UpdateSendResult {
* Generic error while updating a send. The optional [errorMessage] may be displayed directly
* in the UI when present.
*/
data class Error(val errorMessage: String?) : UpdateSendResult()
data class Error(val errorMessage: String?, val error: Throwable?) : UpdateSendResult()
}

View File

@@ -15,25 +15,34 @@ sealed class VaultUnlockResult {
*/
data class AuthenticationError(
val message: String? = null,
override val error: Throwable?,
) : VaultUnlockResult(), VaultUnlockError
/**
* Unable to decode biometrics key.
*/
data object BiometricDecodingError : VaultUnlockResult(), VaultUnlockError
data class BiometricDecodingError(
override val error: Throwable?,
) : VaultUnlockResult(), VaultUnlockError
/**
* Unable to access user state information.
*/
data object InvalidStateError : VaultUnlockResult(), VaultUnlockError
data class InvalidStateError(
override val error: Throwable?,
) : VaultUnlockResult(), VaultUnlockError
/**
* Generic error thrown by Bitwarden SDK.
*/
data object GenericError : VaultUnlockResult(), VaultUnlockError
data class GenericError(
override val error: Throwable?,
) : VaultUnlockResult(), VaultUnlockError
}
/**
* Sealed interface to denote that a [VaultUnlockResult] is an error result.
*/
sealed interface VaultUnlockError
sealed interface VaultUnlockError {
val error: Throwable?
}

View File

@@ -11,6 +11,7 @@ fun InitializeCryptoResult.toVaultUnlockResult(): VaultUnlockResult =
is InitializeCryptoResult.AuthenticationError -> {
VaultUnlockResult.AuthenticationError(
message = this.message,
error = error,
)
}

Some files were not shown because too many files have changed in this diff Show More