mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 19:36:34 -05:00
Compare commits
98 Commits
v2025.1.0-
...
qrcode/1-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
225cb24ac1 | ||
|
|
f4f669683e | ||
|
|
475a82e0fb | ||
|
|
f22156389b | ||
|
|
4f09f5dae4 | ||
|
|
3934bc9ae2 | ||
|
|
72c9149d27 | ||
|
|
a040a38ce8 | ||
|
|
ef3b7730d0 | ||
|
|
ad8d8d271a | ||
|
|
4954e57007 | ||
|
|
6f50fffd17 | ||
|
|
44c5755301 | ||
|
|
b20eece3aa | ||
|
|
869a3b00a5 | ||
|
|
7f8e848c46 | ||
|
|
6c784d28eb | ||
|
|
dfcdc72499 | ||
|
|
db287ddce5 | ||
|
|
9ea85917b1 | ||
|
|
6db4165c4c | ||
|
|
18ce45e7e5 | ||
|
|
6fe9eba620 | ||
|
|
b084987758 | ||
|
|
5d0593026f | ||
|
|
47abeb7843 | ||
|
|
e2779e4edb | ||
|
|
90cc9f77c5 | ||
|
|
40760f270a | ||
|
|
8a773141a4 | ||
|
|
0c149abdd9 | ||
|
|
f540f86b19 | ||
|
|
ca64ce2176 | ||
|
|
da63c9e36b | ||
|
|
e16ad44d5e | ||
|
|
d26a2ee52a | ||
|
|
1eb741ab58 | ||
|
|
e10ca9a6ec | ||
|
|
3fca61ad3e | ||
|
|
409529b9ca | ||
|
|
4568dd53d4 | ||
|
|
b9b90165bf | ||
|
|
778a630012 | ||
|
|
4809066ad7 | ||
|
|
d03c6c243d | ||
|
|
d19ab498ff | ||
|
|
efc3d21fde | ||
|
|
35e585a60e | ||
|
|
39787f9bf0 | ||
|
|
3940997ef9 | ||
|
|
ce482e744d | ||
|
|
b0157d10e2 | ||
|
|
cf3c2fb56d | ||
|
|
a88a173e00 | ||
|
|
ac6ff98041 | ||
|
|
ec030f2c2e | ||
|
|
be08c1a536 | ||
|
|
84edf4ead0 | ||
|
|
915aac561c | ||
|
|
2027a66f02 | ||
|
|
ef6d9bc68c | ||
|
|
3ceda9e40a | ||
|
|
3f1a6e97fd | ||
|
|
4448ab05ce | ||
|
|
d584391843 | ||
|
|
e0d91d7682 | ||
|
|
e8a98dd3ed | ||
|
|
b6163cf53c | ||
|
|
a24c6f2719 | ||
|
|
b3219d4040 | ||
|
|
27043e28a8 | ||
|
|
7c36b7cb82 | ||
|
|
fcfcf48cee | ||
|
|
757994ec18 | ||
|
|
71cd917328 | ||
|
|
5c076871ab | ||
|
|
07d3849c4b | ||
|
|
33c3fd28e9 | ||
|
|
88609c2f5b | ||
|
|
af8cfcd2f0 | ||
|
|
7cc8108498 | ||
|
|
a31c499b15 | ||
|
|
d7d099477f | ||
|
|
0ba240852f | ||
|
|
727d943fae | ||
|
|
234f49a92c | ||
|
|
4f49d3d504 | ||
|
|
aba8344df1 | ||
|
|
6da8e2c47b | ||
|
|
78d5965271 | ||
|
|
6953d5e132 | ||
|
|
7073124495 | ||
|
|
0cc7067808 | ||
|
|
9e920f1cf5 | ||
|
|
537e743891 | ||
|
|
60da236f3e | ||
|
|
7804d8430f | ||
|
|
dab06b0ed4 |
129
.editorconfig
129
.editorconfig
@@ -12,127 +12,20 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
guidelines = 120
|
||||
|
||||
# Code files
|
||||
[*.{cs,csx,vb,vbx}]
|
||||
indent_size = 4
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
|
||||
indent_size = 2
|
||||
|
||||
# JSON files
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
# JS files
|
||||
[*.{js,ts,scss,html}]
|
||||
# Kotlin files
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
[*.{kt,kts}]
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-declaration-site
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-declaration-site
|
||||
trailing-comma-on-declaration-site = true
|
||||
# https://pinterest.github.io/ktlint/1.0.1/rules/configuration-ktlint/#trailing-comma-on-call-site
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||
|
||||
[*.{scss,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{ts}]
|
||||
quote_type = single
|
||||
|
||||
[*.{scss,yml,csproj}]
|
||||
indent_size = 2
|
||||
|
||||
[*.sln]
|
||||
indent_style = tab
|
||||
|
||||
# Dotnet code style settings:
|
||||
[*.{cs,vb}]
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
|
||||
# Use language keywords instead of framework type names for type references
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# Suggest more modern language features when available
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
|
||||
# Prefix private members with underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
|
||||
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
|
||||
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.private_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
|
||||
|
||||
dotnet_naming_style.prefix_underscore.capitalization = camel_case
|
||||
dotnet_naming_style.prefix_underscore.required_prefix = _
|
||||
|
||||
# Async methods should have "Async" suffix
|
||||
dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods
|
||||
dotnet_naming_rule.async_methods_end_in_async.style = end_in_async
|
||||
dotnet_naming_rule.async_methods_end_in_async.severity = suggestion
|
||||
|
||||
dotnet_naming_symbols.any_async_methods.applicable_kinds = method
|
||||
dotnet_naming_symbols.any_async_methods.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.any_async_methods.required_modifiers = async
|
||||
|
||||
dotnet_naming_style.end_in_async.required_prefix =
|
||||
dotnet_naming_style.end_in_async.required_suffix = Async
|
||||
dotnet_naming_style.end_in_async.capitalization = pascal_case
|
||||
dotnet_naming_style.end_in_async.word_separator =
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0618.severity = suggestion
|
||||
|
||||
# Obsolete warnings, this should be removed or changed to warning once we address some of the obsolete items.
|
||||
dotnet_diagnostic.CS0612.severity = suggestion
|
||||
|
||||
# Remove unnecessary using directives https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0005
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# CSharp code style settings:
|
||||
[*.cs]
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_style_var_elsewhere = true:suggestion
|
||||
|
||||
# Prefer method-like constructs to have a expression-body
|
||||
csharp_style_expression_bodied_methods = true:none
|
||||
csharp_style_expression_bodied_constructors = true:none
|
||||
csharp_style_expression_bodied_operators = true:none
|
||||
|
||||
# Prefer property-like constructs to have an expression-body
|
||||
csharp_style_expression_bodied_properties = true:none
|
||||
csharp_style_expression_bodied_indexers = true:none
|
||||
csharp_style_expression_bodied_accessors = true:none
|
||||
|
||||
# Suggest more modern language features when available
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Newline settings
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
|
||||
# Namespace settings
|
||||
csharp_style_namespace_declarations = file_scoped:warning
|
||||
|
||||
# Switch expression
|
||||
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||
|
||||
64
.github/ISSUE_TEMPLATE/bug-passkey.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/bug-passkey.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Passkey Bug Report
|
||||
description: File a Passkey / FIDO2 related bug report
|
||||
labels: [ "app:password-manager", "bug-passkey" ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this Passkey-related bug report!
|
||||
|
||||
Please provide as much detail as possible to help us investigate the issue.
|
||||
|
||||
- type: dropdown
|
||||
id: origin
|
||||
attributes:
|
||||
label: Origin
|
||||
description: Are you using a web browser or a native application?
|
||||
options:
|
||||
- Web (Browser)
|
||||
- Native Application (non-browser app)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: rp-id
|
||||
attributes:
|
||||
label: Web URL or App name
|
||||
description: The website domain or app name you were trying to use the Passkey with
|
||||
placeholder: "e.g. example.com or ExampleApp"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: operation-type
|
||||
attributes:
|
||||
label: Passkey Action
|
||||
description: What passkey related action(s) were you trying to perform?
|
||||
options:
|
||||
- label: Creating new passkey (Registration)
|
||||
- label: Signing in (Authentication)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: build-info
|
||||
attributes:
|
||||
label: Build Information
|
||||
description: Please retrieve the build information from the About screen by tapping the Version number field
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any additional context, steps to reproduce, error messages, or relevant information about the issue
|
||||
|
||||
- type: checkboxes
|
||||
id: issue-tracking-info
|
||||
attributes:
|
||||
label: Issue Tracking Info
|
||||
description: |
|
||||
Issue tracking information
|
||||
options:
|
||||
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
||||
51
.github/renovate.json
vendored
51
.github/renovate.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
27
Gemfile.lock
27
Gemfile.lock
@@ -9,17 +9,18 @@ GEM
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1040.0)
|
||||
aws-sdk-core (3.216.0)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1067.0)
|
||||
aws-sdk-core (3.220.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.97.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.178.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -34,7 +35,7 @@ GEM
|
||||
highline (~> 2.0.0)
|
||||
date (3.4.1)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
@@ -69,7 +70,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
fastlane (2.227.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -137,12 +138,12 @@ GEM
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
@@ -160,15 +161,17 @@ GEM
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.9.1)
|
||||
json (2.10.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
@@ -182,7 +185,7 @@ GEM
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.0)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,11 @@ package com.x8bit.bitwarden
|
||||
const val LEGACY_ACCESSIBILITY_SERVICE_NAME: String =
|
||||
"com.x8bit.bitwarden.Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The short form legacy name for the accessibility service.
|
||||
*/
|
||||
const val LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME: String = ".Accessibility.AccessibilityService"
|
||||
|
||||
/**
|
||||
* The legacy name for the autofill service.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
@@ -15,6 +16,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityCompletionManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillActivityManager
|
||||
@@ -24,16 +26,20 @@ import com.x8bit.bitwarden.data.platform.manager.util.ObserveScreenDataEffect
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavScreen
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
|
||||
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.util.appLanguage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Primary entry point for the application.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@OmitFromCoverage
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@@ -69,13 +75,9 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
// Within the app the language and theme will change dynamically and will be managed by the
|
||||
// Within the app the theme will change dynamically and will be managed by the
|
||||
// OS, but we need to ensure we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
settingsRepository.appLanguage.localeName?.let { localeName ->
|
||||
val localeList = LocaleListCompat.forLanguageTags(localeName)
|
||||
AppCompatDelegate.setApplicationLocales(localeList)
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
|
||||
setContent {
|
||||
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
@@ -111,7 +113,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
|
||||
LocalManagerProvider {
|
||||
LocalManagerProvider(featureFlagsState = state.featureFlagsState) {
|
||||
ObserveScreenDataEffect(
|
||||
onDataUpdate = remember(mainViewModel) {
|
||||
{
|
||||
@@ -122,10 +124,19 @@ class MainActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
BitwardenTheme(theme = state.theme) {
|
||||
RootNavScreen(
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
NavHost(
|
||||
navController = navController,
|
||||
)
|
||||
startDestination = ROOT_ROUTE,
|
||||
) {
|
||||
// Nothing else should end up at this top level, we just want the ability
|
||||
// to have the debug menu appear on top of the rest of the app without
|
||||
// interacting with the state-based navigation used by the RootNavScreen.
|
||||
rootNavDestination { shouldShowSplashScreen = false }
|
||||
debugMenuDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onSplashScreenRemoved = { shouldShowSplashScreen = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +151,31 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// When the app resumes check for any app specific language which may have been
|
||||
// set via the device settings. Similar to the theme setting in onCreate this
|
||||
// ensures we properly set the values when upgrading from older versions
|
||||
// that handle this differently or when the activity restarts.
|
||||
val appSpecificLanguage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val locales: LocaleListCompat = AppCompatDelegate.getApplicationLocales()
|
||||
if (locales.isEmpty) {
|
||||
// App is using the system language
|
||||
null
|
||||
} else {
|
||||
// App has specific language settings
|
||||
locales.get(0)?.appLanguage
|
||||
}
|
||||
} else {
|
||||
// For older versions, use what ever language is available from the repository.
|
||||
settingsRepository.appLanguage
|
||||
}
|
||||
|
||||
appSpecificLanguage?.let {
|
||||
mainViewModel.trySendAction(MainAction.AppSpecificLanguageUpdate(it))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// In some scenarios on an emulator the Activity can leak when recreated
|
||||
|
||||
@@ -19,10 +19,12 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
@@ -32,8 +34,10 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.model.FeatureFlagsState
|
||||
import com.x8bit.bitwarden.ui.platform.util.isAccountSecurityShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
|
||||
@@ -54,6 +58,7 @@ import java.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
private const val ANIMATION_REFRESH_DELAY = 500L
|
||||
|
||||
/**
|
||||
* A view model that helps launch actions for the [MainActivity].
|
||||
@@ -63,12 +68,13 @@ private const val SPECIAL_CIRCUMSTANCE_KEY = "special-circumstance"
|
||||
class MainViewModel @Inject constructor(
|
||||
accessibilitySelectionManager: AccessibilitySelectionManager,
|
||||
autofillSelectionManager: AutofillSelectionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
private val addTotpItemFromAuthenticatorManager: AddTotpItemFromAuthenticatorManager,
|
||||
private val specialCircumstanceManager: SpecialCircumstanceManager,
|
||||
private val garbageCollectionManager: GarbageCollectionManager,
|
||||
private val fido2CredentialManager: Fido2CredentialManager,
|
||||
private val intentManager: IntentManager,
|
||||
settingsRepository: SettingsRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
@@ -79,6 +85,9 @@ class MainViewModel @Inject constructor(
|
||||
initialState = MainState(
|
||||
theme = settingsRepository.appTheme,
|
||||
isScreenCaptureAllowed = settingsRepository.isScreenCaptureAllowed,
|
||||
isErrorReportingDialogEnabled = featureFlagManager.getFeatureFlag(
|
||||
key = FlagKey.MobileErrorReporting,
|
||||
),
|
||||
),
|
||||
) {
|
||||
private var specialCircumstance: SpecialCircumstance?
|
||||
@@ -96,6 +105,12 @@ class MainViewModel @Inject constructor(
|
||||
.onEach { specialCircumstance = it }
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
featureFlagManager
|
||||
.getFeatureFlagFlow(key = FlagKey.MobileErrorReporting)
|
||||
.map { MainAction.Internal.OnMobileErrorReportingReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
accessibilitySelectionManager
|
||||
.accessibilitySelectionFlow
|
||||
.map { MainAction.Internal.AccessibilitySelectionReceive(it) }
|
||||
@@ -134,8 +149,7 @@ class MainViewModel @Inject constructor(
|
||||
// Switching between account states often involves some kind of animation (ex:
|
||||
// account switcher) that we might want to give time to finish before triggering
|
||||
// a refresh.
|
||||
@Suppress("MagicNumber")
|
||||
delay(500)
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.CurrentUserStateChange)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
@@ -147,8 +161,7 @@ class MainViewModel @Inject constructor(
|
||||
is VaultStateEvent.Locked -> {
|
||||
// Similar to account switching, triggering this action too soon can
|
||||
// interfere with animations or navigation logic, so we will delay slightly.
|
||||
@Suppress("MagicNumber")
|
||||
delay(500)
|
||||
delay(ANIMATION_REFRESH_DELAY)
|
||||
trySendAction(MainAction.Internal.VaultUnlockStateChange)
|
||||
}
|
||||
|
||||
@@ -172,6 +185,17 @@ class MainViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
override fun handleAction(action: MainAction) {
|
||||
when (action) {
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
is MainAction.AppSpecificLanguageUpdate -> handleAppSpecificLanguageUpdate(action)
|
||||
is MainAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: MainAction.Internal) {
|
||||
when (action) {
|
||||
is MainAction.Internal.AccessibilitySelectionReceive -> {
|
||||
handleAccessibilitySelectionReceive(action)
|
||||
@@ -185,13 +209,24 @@ class MainViewModel @Inject constructor(
|
||||
is MainAction.Internal.ScreenCaptureUpdate -> handleScreenCaptureUpdate(action)
|
||||
is MainAction.Internal.ThemeUpdate -> handleAppThemeUpdated(action)
|
||||
is MainAction.Internal.VaultUnlockStateChange -> handleVaultUnlockStateChange()
|
||||
is MainAction.ReceiveFirstIntent -> handleFirstIntentReceived(action)
|
||||
is MainAction.ReceiveNewIntent -> handleNewIntentReceived(action)
|
||||
MainAction.OpenDebugMenu -> handleOpenDebugMenu()
|
||||
is MainAction.ResumeScreenDataReceived -> handleAppResumeDataUpdated(action)
|
||||
is MainAction.Internal.OnMobileErrorReportingReceive -> {
|
||||
handleOnMobileErrorReportingReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnMobileErrorReportingReceive(
|
||||
action: MainAction.Internal.OnMobileErrorReportingReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isErrorReportingDialogEnabled = action.isErrorReportingEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAppSpecificLanguageUpdate(action: MainAction.AppSpecificLanguageUpdate) {
|
||||
settingsRepository.appLanguage = action.appLanguage
|
||||
}
|
||||
|
||||
private fun handleAppResumeDataUpdated(action: MainAction.ResumeScreenDataReceived) {
|
||||
when (val data = action.screenResumeData) {
|
||||
null -> appResumeManager.clearResumeScreen()
|
||||
@@ -445,7 +480,16 @@ class MainViewModel @Inject constructor(
|
||||
data class MainState(
|
||||
val theme: AppTheme,
|
||||
val isScreenCaptureAllowed: Boolean,
|
||||
) : Parcelable
|
||||
private val isErrorReportingDialogEnabled: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Contains all feature flags that are available to the UI.
|
||||
*/
|
||||
val featureFlagsState: FeatureFlagsState
|
||||
get() = FeatureFlagsState(
|
||||
isErrorReportingDialogEnabled = isErrorReportingDialogEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the [MainActivity].
|
||||
@@ -471,6 +515,12 @@ sealed class MainAction {
|
||||
*/
|
||||
data class ResumeScreenDataReceived(val screenResumeData: AppResumeScreenData?) : MainAction()
|
||||
|
||||
/**
|
||||
* Receive if there is an app specific locale selection made by user
|
||||
* in the device's settings.
|
||||
*/
|
||||
data class AppSpecificLanguageUpdate(val appLanguage: AppLanguage) : MainAction()
|
||||
|
||||
/**
|
||||
* Actions for internal use by the ViewModel.
|
||||
*/
|
||||
@@ -483,6 +533,13 @@ sealed class MainAction {
|
||||
val cipherView: CipherView,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the Mobile Error Reporting feature flag has been updated.
|
||||
*/
|
||||
data class OnMobileErrorReportingReceive(
|
||||
val isErrorReportingEnabled: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates the user has manually selected the given [cipherView] for autofill.
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestsUpdatesResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.isSso
|
||||
import com.x8bit.bitwarden.data.auth.manager.util.toAuthRequestTypeJson
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
@@ -51,7 +52,9 @@ class AuthRequestManagerImpl(
|
||||
override fun getAuthRequestsWithUpdates(): Flow<AuthRequestsUpdatesResult> = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
when (val result = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> emit(AuthRequestsUpdatesResult.Error)
|
||||
is AuthRequestsResult.Error -> {
|
||||
emit(AuthRequestsUpdatesResult.Error(error = result.error))
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
emit(AuthRequestsUpdatesResult.Update(authRequests = result.authRequests))
|
||||
@@ -70,9 +73,8 @@ class AuthRequestManagerImpl(
|
||||
email = email,
|
||||
authRequestType = authRequestType.toAuthRequestTypeJson(),
|
||||
)
|
||||
.getOrNull()
|
||||
?: run {
|
||||
emit(CreateAuthRequestResult.Error)
|
||||
.getOrElse {
|
||||
emit(CreateAuthRequestResult.Error(error = it))
|
||||
return@flow
|
||||
}
|
||||
var authRequest = initialResult.authRequest
|
||||
@@ -103,7 +105,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { emit(CreateAuthRequestResult.Error) },
|
||||
onFailure = { emit(CreateAuthRequestResult.Error(error = it)) },
|
||||
onSuccess = { updateAuthRequest ->
|
||||
when {
|
||||
updateAuthRequest.requestApproved -> {
|
||||
@@ -182,7 +184,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error) },
|
||||
onFailure = { emit(AuthRequestUpdatesResult.Error(error = it)) },
|
||||
onSuccess = { updateAuthRequest ->
|
||||
when {
|
||||
updateAuthRequest.requestApproved -> {
|
||||
@@ -218,13 +220,18 @@ class AuthRequestManagerImpl(
|
||||
fingerprint: String,
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
when (val authRequestsResult = getAuthRequests()) {
|
||||
AuthRequestsResult.Error -> AuthRequestUpdatesResult.Error
|
||||
is AuthRequestsResult.Error -> {
|
||||
AuthRequestUpdatesResult.Error(error = authRequestsResult.error)
|
||||
}
|
||||
|
||||
is AuthRequestsResult.Success -> {
|
||||
authRequestsResult
|
||||
.authRequests
|
||||
.firstOrNull { it.fingerprint == fingerprint }
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
?: AuthRequestUpdatesResult.Error(
|
||||
error = IllegalStateException("Could not find the auth request."),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,30 +241,28 @@ class AuthRequestManagerImpl(
|
||||
): Flow<AuthRequestUpdatesResult> = getAuthRequest {
|
||||
authRequestsService
|
||||
.getAuthRequest(requestId)
|
||||
.map { response ->
|
||||
getFingerprintPhrase(response.publicKey).getOrNull()?.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
.mapCatching { response ->
|
||||
getFingerprintPhrase(response.publicKey)
|
||||
.getOrThrow()
|
||||
.let { fingerprint ->
|
||||
AuthRequest(
|
||||
id = response.id,
|
||||
publicKey = response.publicKey,
|
||||
platform = response.platform,
|
||||
ipAddress = response.ipAddress,
|
||||
key = response.key,
|
||||
masterPasswordHash = response.masterPasswordHash,
|
||||
creationDate = response.creationDate,
|
||||
responseDate = response.responseDate,
|
||||
requestApproved = response.requestApproved ?: false,
|
||||
originUrl = response.originUrl,
|
||||
fingerprint = fingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestUpdatesResult.Error },
|
||||
onSuccess = { authRequest ->
|
||||
authRequest
|
||||
?.let { AuthRequestUpdatesResult.Update(it) }
|
||||
?: AuthRequestUpdatesResult.Error
|
||||
},
|
||||
onFailure = { AuthRequestUpdatesResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestUpdatesResult.Update(authRequest = it) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,7 +314,7 @@ class AuthRequestManagerImpl(
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestsResult.Error },
|
||||
onFailure = { AuthRequestsResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestsResult.Success(authRequests = it) },
|
||||
)
|
||||
|
||||
@@ -319,7 +324,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
isApproved: Boolean,
|
||||
): AuthRequestResult {
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error
|
||||
val userId = activeUserId ?: return AuthRequestResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.getAuthRequestKey(
|
||||
publicKey = publicKey,
|
||||
@@ -350,7 +355,7 @@ class AuthRequestManagerImpl(
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = { AuthRequestResult.Error },
|
||||
onFailure = { AuthRequestResult.Error(error = it) },
|
||||
onSuccess = { AuthRequestResult.Success(authRequest = it) },
|
||||
)
|
||||
}
|
||||
@@ -462,7 +467,7 @@ class AuthRequestManagerImpl(
|
||||
publicKey: String,
|
||||
): Result<String> {
|
||||
val profile = authDiskSource.userState?.activeAccount?.profile
|
||||
?: return IllegalStateException("No active account").asFailure()
|
||||
?: return NoActiveUserException().asFailure()
|
||||
return authSdkSource.getUserFingerprint(
|
||||
email = profile.email,
|
||||
publicKey = publicKey,
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestResult()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ sealed class AuthRequestUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestUpdatesResult()
|
||||
|
||||
/**
|
||||
* The auth request has been declined.
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class AuthRequestsUpdatesResult {
|
||||
/**
|
||||
* There was an error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : AuthRequestsUpdatesResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : AuthRequestsUpdatesResult()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ sealed class CreateAuthRequestResult {
|
||||
/**
|
||||
* There was a generic error getting the user's auth requests.
|
||||
*/
|
||||
data object Error : CreateAuthRequestResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : CreateAuthRequestResult()
|
||||
|
||||
/**
|
||||
* The auth request has been declined.
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ sealed class BreachCountResult {
|
||||
/**
|
||||
* There was an error determining if the password has been breached.
|
||||
*/
|
||||
data object Error : BreachCountResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
val message: String? = null,
|
||||
) : BreachCountResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@ sealed class DeleteAccountResult {
|
||||
/**
|
||||
* There was an error deleting the account.
|
||||
*/
|
||||
data class Error(val message: String?) : DeleteAccountResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : DeleteAccountResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ sealed class EmailTokenResult {
|
||||
/**
|
||||
* There was an error validating the token.
|
||||
*/
|
||||
data class Error(val message: String?) : EmailTokenResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : EmailTokenResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class KnownDeviceResult {
|
||||
/**
|
||||
* There was an error determining if this is a known device.
|
||||
*/
|
||||
data object Error : KnownDeviceResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : KnownDeviceResult()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ sealed class LoginResult {
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : LoginResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error while logging into an unofficial Bitwarden server.
|
||||
|
||||
@@ -8,9 +8,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
* the necessary `message` if applicable.
|
||||
*/
|
||||
fun VaultUnlockError.toLoginErrorResult(): LoginResult.Error = when (this) {
|
||||
is VaultUnlockResult.AuthenticationError -> LoginResult.Error(this.message)
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null)
|
||||
is VaultUnlockResult.AuthenticationError -> {
|
||||
LoginResult.Error(errorMessage = this.message, error = this.error)
|
||||
}
|
||||
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> LoginResult.Error(errorMessage = null, error = this.error)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class NewSsoUserResult {
|
||||
/**
|
||||
* There was an error while truing to create the new user.
|
||||
*/
|
||||
data object Failure : NewSsoUserResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : NewSsoUserResult()
|
||||
}
|
||||
|
||||
@@ -22,5 +22,7 @@ sealed class OrganizationDomainSsoDetailsResult {
|
||||
/**
|
||||
* The request failed.
|
||||
*/
|
||||
data object Failure : OrganizationDomainSsoDetailsResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : OrganizationDomainSsoDetailsResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class PasswordHintResult {
|
||||
/**
|
||||
* There was an error.
|
||||
*/
|
||||
data class Error(val message: String?) : PasswordHintResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable?,
|
||||
) : PasswordHintResult()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,7 @@ sealed class PasswordStrengthResult {
|
||||
/**
|
||||
* There was an error determining the password strength.
|
||||
*/
|
||||
data object Error : PasswordStrengthResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : PasswordStrengthResult()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ sealed class PrevalidateSsoResult {
|
||||
*/
|
||||
data class Failure(
|
||||
val message: String? = null,
|
||||
val error: Throwable?,
|
||||
) : PrevalidateSsoResult()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ sealed class RegisterResult {
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : RegisterResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : RegisterResult()
|
||||
|
||||
/**
|
||||
* Password hash was found in a data breach.
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class RemovePasswordResult {
|
||||
/**
|
||||
* There was an error removing the password.
|
||||
*/
|
||||
data object Error : RemovePasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : RemovePasswordResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class RequestOtpResult {
|
||||
/**
|
||||
* Represents a failure to send the one-time passcode.
|
||||
*/
|
||||
data class Error(val message: String?) : RequestOtpResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : RequestOtpResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class ResendEmailResult {
|
||||
/**
|
||||
* There was an error.
|
||||
*/
|
||||
data class Error(val message: String?) : ResendEmailResult()
|
||||
data class Error(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : ResendEmailResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class ResetPasswordResult {
|
||||
/**
|
||||
* There was an error resetting the password.
|
||||
*/
|
||||
data object Error : ResetPasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ResetPasswordResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,8 @@ sealed class SendVerificationEmailResult {
|
||||
*
|
||||
* @param errorMessage a message describing the error.
|
||||
*/
|
||||
data class Error(val errorMessage: String?) : SendVerificationEmailResult()
|
||||
data class Error(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable?,
|
||||
) : SendVerificationEmailResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class SetPasswordResult {
|
||||
/**
|
||||
* There was an error setting the password.
|
||||
*/
|
||||
data object Error : SetPasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : SetPasswordResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class UserFingerprintResult {
|
||||
/**
|
||||
* There was an error getting the user fingerprint.
|
||||
*/
|
||||
data object Error : UserFingerprintResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : UserFingerprintResult()
|
||||
}
|
||||
|
||||
@@ -15,5 +15,7 @@ sealed class ValidatePasswordResult {
|
||||
/**
|
||||
* There was an error determining if the validity of the password.
|
||||
*/
|
||||
data object Error : ValidatePasswordResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ValidatePasswordResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class ValidatePinResult {
|
||||
/**
|
||||
* There was an error determining if the validity of the PIN.
|
||||
*/
|
||||
data object Error : ValidatePinResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : ValidatePinResult()
|
||||
}
|
||||
|
||||
@@ -18,5 +18,7 @@ sealed class VerifiedOrganizationDomainSsoDetailsResult {
|
||||
/**
|
||||
* The request failed.
|
||||
*/
|
||||
data object Failure : VerifiedOrganizationDomainSsoDetailsResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : VerifiedOrganizationDomainSsoDetailsResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,8 @@ sealed class VerifyOtpResult {
|
||||
/**
|
||||
* Represents a failure to verify the one-time passcode.
|
||||
*/
|
||||
data class NotVerified(val errorMessage: String?) : VerifyOtpResult()
|
||||
data class NotVerified(
|
||||
val errorMessage: String?,
|
||||
val error: Throwable,
|
||||
) : VerifyOtpResult()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.content.Intent
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import androidx.annotation.Keep
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.processor.BitwardenAccessibilityProcessor
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.tiles.BitwardenAutofillTileService
|
||||
@@ -21,9 +23,28 @@ class BitwardenAccessibilityService : AccessibilityService() {
|
||||
@Inject
|
||||
lateinit var processor: BitwardenAccessibilityProcessor
|
||||
|
||||
@Inject
|
||||
lateinit var accessibilityEnabledManager: AccessibilityEnabledManager
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
processor.processAccessibilityEvent(event = event) { rootInActiveWindow }
|
||||
}
|
||||
|
||||
override fun onInterrupt() = Unit
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent?): Boolean {
|
||||
return super
|
||||
.onUnbind(intent)
|
||||
.also { accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings() }
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
accessibilityEnabledManager.refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ object AccessibilityModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesAccessibilityEnabledManager(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
@ApplicationContext context: Context,
|
||||
): AccessibilityEnabledManager =
|
||||
AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager = accessibilityManager,
|
||||
context = context,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@@ -10,4 +10,9 @@ interface AccessibilityEnabledManager {
|
||||
* Emits updates that track whether the accessibility autofill service is enabled..
|
||||
*/
|
||||
val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* Gets the accessibility enabled state from the system settings.
|
||||
*/
|
||||
fun refreshAccessibilityEnabledFromSettings()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.accessibility.manager
|
||||
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import android.content.Context
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.util.isAccessibilityServiceEnabled
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -9,20 +10,20 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
* The default implementation of [AccessibilityEnabledManager].
|
||||
*/
|
||||
class AccessibilityEnabledManagerImpl(
|
||||
accessibilityManager: AccessibilityManager,
|
||||
private val context: Context,
|
||||
) : AccessibilityEnabledManager {
|
||||
private val mutableIsAccessibilityEnabledStateFlow = MutableStateFlow(
|
||||
value = accessibilityManager.isEnabled,
|
||||
value = context.isAccessibilityServiceEnabled,
|
||||
)
|
||||
|
||||
init {
|
||||
accessibilityManager.addAccessibilityStateChangeListener(
|
||||
AccessibilityManager.AccessibilityStateChangeListener { isEnabled ->
|
||||
mutableIsAccessibilityEnabledStateFlow.value = isEnabled
|
||||
},
|
||||
)
|
||||
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
|
||||
}
|
||||
|
||||
override val isAccessibilityEnabledStateFlow: StateFlow<Boolean>
|
||||
get() = mutableIsAccessibilityEnabledStateFlow.asStateFlow()
|
||||
|
||||
override fun refreshAccessibilityEnabledFromSettings() {
|
||||
mutableIsAccessibilityEnabledStateFlow.value = context.isAccessibilityServiceEnabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.data.autofill.accessibility.util
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import com.x8bit.bitwarden.LEGACY_ACCESSIBILITY_SERVICE_NAME
|
||||
import com.x8bit.bitwarden.LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilityService
|
||||
import com.x8bit.bitwarden.data.autofill.util.containsAnyTerms
|
||||
|
||||
/**
|
||||
* Helper method to determine if the [BitwardenAccessibilityService] is enabled.
|
||||
@@ -11,16 +13,25 @@ import com.x8bit.bitwarden.data.autofill.accessibility.BitwardenAccessibilitySer
|
||||
val Context.isAccessibilityServiceEnabled: Boolean
|
||||
get() {
|
||||
val appContext = this.applicationContext
|
||||
val accessibilityServiceName = appContext
|
||||
.packageName
|
||||
?.let { "$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME" }
|
||||
?: return false
|
||||
val packageName = appContext.packageName
|
||||
val accessibilityServiceName = packageName?.let {
|
||||
"$it/$LEGACY_ACCESSIBILITY_SERVICE_NAME"
|
||||
}
|
||||
val shortAccessibilityServiceName = packageName.let {
|
||||
"$it/$LEGACY_SHORT_ACCESSIBILITY_SERVICE_NAME"
|
||||
}
|
||||
return Settings
|
||||
.Secure
|
||||
.getString(
|
||||
appContext.contentResolver,
|
||||
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
|
||||
)
|
||||
?.contains(accessibilityServiceName)
|
||||
?.containsAnyTerms(
|
||||
terms = listOfNotNull(
|
||||
accessibilityServiceName,
|
||||
shortAccessibilityServiceName,
|
||||
),
|
||||
ignoreCase = true,
|
||||
)
|
||||
?: false
|
||||
}
|
||||
|
||||
@@ -134,7 +134,6 @@ class Fido2OriginManagerImpl(
|
||||
target.packageName == rpPackageName &&
|
||||
statement.relation.containsAll(
|
||||
listOf(
|
||||
"delegate_permission/common.get_login_creds",
|
||||
"delegate_permission/common.handle_all_urls",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -267,7 +267,7 @@ class Fido2ProviderProcessorImpl(
|
||||
val result = vaultRepository
|
||||
.getDecryptedFido2CredentialAutofillViews(cipherViews)
|
||||
return when (result) {
|
||||
DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
is DecryptFido2CredentialAutofillViewResult.Error -> {
|
||||
throw GetCredentialUnknownException("Error decrypting credentials.")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that a required property was missing.
|
||||
*/
|
||||
class MissingPropertyException(
|
||||
propertyName: String,
|
||||
) : IllegalStateException("Missing the required $propertyName property")
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.error
|
||||
|
||||
/**
|
||||
* An exception indicating that there is currently no active user when one is required.
|
||||
*/
|
||||
class NoActiveUserException : IllegalStateException("No current active user!")
|
||||
@@ -158,7 +158,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()
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.security.KeyChain
|
||||
import android.security.KeyChainException
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import com.x8bit.bitwarden.data.platform.error.MissingPropertyException
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
@@ -24,7 +25,7 @@ class KeyManagerImpl(
|
||||
private val context: Context,
|
||||
) : KeyManager {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
override fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
@@ -35,28 +36,29 @@ class KeyManagerImpl(
|
||||
.inputStream()
|
||||
.use { stream ->
|
||||
try {
|
||||
KeyStore.getInstance(KEYSTORE_TYPE_PKCS12)
|
||||
KeyStore
|
||||
.getInstance(KEYSTORE_TYPE_PKCS12)
|
||||
.also { it.load(stream, password.toCharArray()) }
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to load PKCS12 bytes")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
|
||||
return when (e.cause) {
|
||||
is UnrecoverableKeyException -> {
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
|
||||
}
|
||||
}
|
||||
} catch (e: CertificateException) {
|
||||
Timber.Forest.e(e, "Unable to load certificate chain")
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain(throwable = e)
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Cryptographic algorithm not supported")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,22 +66,29 @@ class KeyManagerImpl(
|
||||
val internalAlias = pkcs12KeyStore.aliases()
|
||||
?.takeIf { it.hasMoreElements() }
|
||||
?.nextElement()
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey(
|
||||
throwable = MissingPropertyException("Internal Alias"),
|
||||
)
|
||||
|
||||
// Step 3: Extract PrivateKey and X.509 certificate from the KeyStore and verify
|
||||
// certificate alias.
|
||||
val privateKey = try {
|
||||
pkcs12KeyStore.getKey(internalAlias, password.toCharArray())
|
||||
?: return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
pkcs12KeyStore
|
||||
.getKey(internalAlias, password.toCharArray())
|
||||
?: return ImportPrivateKeyResult.Error.UnrecoverableKey(
|
||||
throwable = MissingPropertyException("Private Key"),
|
||||
)
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to get private key")
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = e)
|
||||
}
|
||||
|
||||
val certChain: Array<Certificate> = pkcs12KeyStore
|
||||
.getCertificateChain(internalAlias)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain(
|
||||
throwable = MissingPropertyException("Certificate Chain"),
|
||||
)
|
||||
|
||||
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
|
||||
// does not exists.
|
||||
@@ -92,7 +101,7 @@ class KeyManagerImpl(
|
||||
setKeyEntry(alias, privateKey, null, certChain)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to import key into Android KeyStore")
|
||||
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
return ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = e)
|
||||
}
|
||||
}
|
||||
return ImportPrivateKeyResult.Success(alias)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
/**
|
||||
* Manager for loading native libraries.
|
||||
*/
|
||||
interface NativeLibraryManager {
|
||||
|
||||
/**
|
||||
* Loads a native library with the given [libraryName].
|
||||
*/
|
||||
fun loadLibrary(libraryName: String): Result<Unit>
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Primary implementation of [NativeLibraryManager].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class NativeLibraryManagerImpl : NativeLibraryManager {
|
||||
override fun loadLibrary(libraryName: String): Result<Unit> {
|
||||
return try {
|
||||
System.loadLibrary(libraryName)
|
||||
Result.success(Unit)
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
Timber.e(e, "Failed to load native library $libraryName.")
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,6 @@ class PolicyManagerImpl(
|
||||
.getOrganizations(userId)
|
||||
?.filter {
|
||||
it.shouldUsePolicies &&
|
||||
it.isEnabled &&
|
||||
it.status >= OrganizationStatusType.ACCEPTED &&
|
||||
!isOrganizationExemptFromPolicies(it, type)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import android.os.Build
|
||||
import com.bitwarden.sdk.Client
|
||||
import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
|
||||
|
||||
/**
|
||||
* Primary implementation of [SdkClientManager].
|
||||
*/
|
||||
class SdkClientManagerImpl(
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
private val clientProvider: suspend () -> Client = {
|
||||
Client(settings = null).apply {
|
||||
platform().loadFlags(featureFlagManager.sdkFeatureFlags)
|
||||
@@ -15,6 +18,15 @@ class SdkClientManagerImpl(
|
||||
) : SdkClientManager {
|
||||
private val userIdToClientMap = mutableMapOf<String?, Client>()
|
||||
|
||||
init {
|
||||
// The SDK requires access to Android APIs that were not made public until API 31. In order
|
||||
// to work around this limitation the SDK must be manually loaded prior to initializing any
|
||||
// [Client] instance.
|
||||
if (isBuildVersionBelow(Build.VERSION_CODES.S)) {
|
||||
nativeLibraryManager.loadLibrary("bitwarden_uniffi")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateClient(
|
||||
userId: String?,
|
||||
): Client = userIdToClientMap.getOrPut(key = userId) { clientProvider() }
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.NativeLibraryManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
@@ -193,12 +195,18 @@ object PlatformManagerModule {
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSdkClientManager(
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
nativeLibraryManager: NativeLibraryManager,
|
||||
): SdkClientManager = SdkClientManagerImpl(
|
||||
featureFlagManager = featureFlagManager,
|
||||
nativeLibraryManager = nativeLibraryManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -16,30 +16,44 @@ sealed class ImportPrivateKeyResult {
|
||||
* Represents a generic error during the import process.
|
||||
*/
|
||||
sealed class Error : ImportPrivateKeyResult() {
|
||||
/**
|
||||
* The underlying error.
|
||||
*/
|
||||
abstract val throwable: Throwable?
|
||||
|
||||
/**
|
||||
* Indicates that the provided key is unrecoverable or the password is incorrect.
|
||||
*/
|
||||
data object UnrecoverableKey : Error()
|
||||
data class UnrecoverableKey(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the certificate chain associated with the key is invalid.
|
||||
*/
|
||||
data object InvalidCertificateChain : Error()
|
||||
data class InvalidCertificateChain(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the specified alias is already in use.
|
||||
*/
|
||||
data object DuplicateAlias : Error()
|
||||
data object DuplicateAlias : Error() {
|
||||
override val throwable: Throwable? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred during the key store operation.
|
||||
*/
|
||||
data object KeyStoreOperationFailed : Error()
|
||||
data class KeyStoreOperationFailed(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
|
||||
/**
|
||||
* Indicates the provided key is not supported.
|
||||
*/
|
||||
data object UnsupportedKey : Error()
|
||||
data class UnsupportedKey(
|
||||
override val throwable: Throwable,
|
||||
) : Error()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ package com.x8bit.bitwarden.data.platform.manager.model
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.manager.PushManager
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,7 @@ import java.time.ZonedDateTime
|
||||
* Note: The data we receive is not always reliable, so everything is nullable and we validate the
|
||||
* data in the [PushManager] as necessary.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
sealed class NotificationPayload {
|
||||
/**
|
||||
@@ -24,12 +26,12 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncCipherNotification(
|
||||
@SerialName("Id") val cipherId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@SerialName("OrganizationId") val organizationId: String?,
|
||||
@SerialName("CollectionIds") val collectionIds: List<String>?,
|
||||
@JsonNames("Id", "id") val cipherId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("OrganizationId", "organizationId") val organizationId: String?,
|
||||
@JsonNames("CollectionIds", "collectionIds") val collectionIds: List<String>?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -37,10 +39,10 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncFolderNotification(
|
||||
@SerialName("Id") val folderId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val folderId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -48,9 +50,9 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class UserNotification(
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("Date") val date: ZonedDateTime?,
|
||||
@JsonNames("Date", "date") val date: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -58,10 +60,10 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class SyncSendNotification(
|
||||
@SerialName("Id") val sendId: String?,
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val sendId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@Contextual
|
||||
@SerialName("RevisionDate") val revisionDate: ZonedDateTime?,
|
||||
@JsonNames("RevisionDate", "revisionDate") val revisionDate: ZonedDateTime?,
|
||||
) : NotificationPayload()
|
||||
|
||||
/**
|
||||
@@ -69,7 +71,7 @@ sealed class NotificationPayload {
|
||||
*/
|
||||
@Serializable
|
||||
data class PasswordlessRequestNotification(
|
||||
@SerialName("UserId") override val userId: String?,
|
||||
@SerialName("Id") val loginRequestId: String?,
|
||||
@JsonNames("UserId", "userId") override val userId: String?,
|
||||
@JsonNames("Id", "id") val loginRequestId: String?,
|
||||
) : NotificationPayload()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.repository
|
||||
import com.bitwarden.authenticatorbridge.model.SharedAccountData
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.sanitizeTotpUri
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -71,9 +72,9 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
|
||||
when (unlockResult) {
|
||||
is VaultUnlockResult.AuthenticationError,
|
||||
VaultUnlockResult.BiometricDecodingError,
|
||||
VaultUnlockResult.GenericError,
|
||||
VaultUnlockResult.InvalidStateError,
|
||||
is VaultUnlockResult.BiometricDecodingError,
|
||||
is VaultUnlockResult.GenericError,
|
||||
is VaultUnlockResult.InvalidStateError,
|
||||
-> {
|
||||
// Not being able to unlock the user's vault with the
|
||||
// decrypted unlock key is an unexpected case, but if it does
|
||||
@@ -96,18 +97,21 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
val totpUris = vaultDiskSource
|
||||
.getCiphers(userId)
|
||||
.first()
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers:
|
||||
// Filter out any ciphers without a totp item and also deleted ciphers
|
||||
.filter { it.login?.totp != null && it.deletedDate == null }
|
||||
.mapNotNull {
|
||||
// Decrypt each cipher and take just totp codes:
|
||||
vaultSdkSource
|
||||
val decryptedCipher = vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = userId,
|
||||
cipher = it.toEncryptedSdkCipher(),
|
||||
)
|
||||
.getOrNull()
|
||||
?.login
|
||||
?.totp
|
||||
|
||||
val rawTotp = decryptedCipher?.login?.totp
|
||||
val cipherName = decryptedCipher?.name
|
||||
val username = decryptedCipher?.login?.username
|
||||
|
||||
rawTotp.sanitizeTotpUri(cipherName, username)
|
||||
}
|
||||
|
||||
// Lock the user's vault if we unlocked it for this operation:
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
|
||||
import com.x8bit.bitwarden.data.autofill.accessibility.manager.AccessibilityEnabledManager
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
|
||||
@@ -380,12 +381,13 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun getUserFingerprint(): UserFingerprintResult {
|
||||
val userId = activeUserId ?: return UserFingerprintResult.Error
|
||||
val userId = activeUserId
|
||||
?: return UserFingerprintResult.Error(error = NoActiveUserException())
|
||||
|
||||
return vaultSdkSource
|
||||
.getUserFingerprint(userId)
|
||||
.fold(
|
||||
onFailure = { UserFingerprintResult.Error },
|
||||
onFailure = { UserFingerprintResult.Error(error = it) },
|
||||
onSuccess = { UserFingerprintResult.Success(it) },
|
||||
)
|
||||
}
|
||||
@@ -492,7 +494,8 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun setupBiometricsKey(cipher: Cipher): BiometricsKeyResult {
|
||||
val userId = activeUserId ?: return BiometricsKeyResult.Error
|
||||
val userId = activeUserId
|
||||
?: return BiometricsKeyResult.Error(error = NoActiveUserException())
|
||||
return vaultSdkSource
|
||||
.getUserEncryptionKey(userId = userId)
|
||||
.onSuccess { biometricsKey ->
|
||||
@@ -506,7 +509,7 @@ class SettingsRepositoryImpl(
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { BiometricsKeyResult.Success },
|
||||
onFailure = { BiometricsKeyResult.Error },
|
||||
onFailure = { BiometricsKeyResult.Error(error = it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,7 @@ sealed class BiometricsKeyResult {
|
||||
/**
|
||||
* Generic error while setting up the biometrics key.
|
||||
*/
|
||||
data object Error : BiometricsKeyResult()
|
||||
data class Error(
|
||||
val error: Throwable,
|
||||
) : BiometricsKeyResult()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.x8bit.bitwarden.data.platform.repository.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
|
||||
private const val OTPAUTH_PREFIX = "otpauth://totp/"
|
||||
private const val STEAM_PREFIX = "steam://"
|
||||
|
||||
/**
|
||||
* Utility for ensuring that a given TOTP string is a properly formatted otpauth:// or steam:// URI.
|
||||
* If the input TOTP is already a valid URI, it is returned as-is.
|
||||
* If the TOTP is manually entered and does not follow the URI format,
|
||||
* this function reconstructs it using the provided issuer and username.
|
||||
*
|
||||
* Uses this as a guide for format
|
||||
* https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
*
|
||||
* Replace spaces (+) with %20, and encode the label and issuer (per the above link)
|
||||
* https://datatracker.ietf.org/doc/html/rfc5234
|
||||
* */
|
||||
fun String?.sanitizeTotpUri(
|
||||
issuer: String?,
|
||||
username: String?,
|
||||
): String? {
|
||||
if (this.isNullOrBlank()) return null
|
||||
|
||||
return if (this.startsWith(OTPAUTH_PREFIX) || this.startsWith(STEAM_PREFIX)) {
|
||||
// ✅ Already a valid TOTP or Steam URI, return as-is.
|
||||
this
|
||||
} else {
|
||||
// ❌ Manually entered secret, reconstruct as otpauth://totp/ URI.
|
||||
|
||||
// Trim spaces from issuer and username
|
||||
val trimmedIssuer = issuer
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val trimmedUsername = username
|
||||
?.trim()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
// Determine raw label correctly (avoid empty `:` issue)
|
||||
val rawLabel = if (trimmedIssuer != null && trimmedUsername != null) {
|
||||
"$trimmedIssuer:$trimmedUsername"
|
||||
} else {
|
||||
trimmedUsername
|
||||
}
|
||||
|
||||
// Encode label only if it's not empty
|
||||
val encodedLabel = rawLabel
|
||||
?.let {
|
||||
URLEncoder
|
||||
.encode(it, "UTF-8")
|
||||
.replace("+", "%20")
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
// Encode issuer separately for the query parameter
|
||||
val encodedIssuer = trimmedIssuer?.let {
|
||||
URLEncoder
|
||||
.encode(it, "UTF-8")
|
||||
.replace("+", "%20")
|
||||
}
|
||||
|
||||
// Construct the issuer query parameter.
|
||||
val issuerParameter = encodedIssuer
|
||||
?.let { "&issuer=$it" }
|
||||
.orEmpty()
|
||||
|
||||
// Remove spaces from the manually entered secret
|
||||
val sanitizedSecret = this.filterNot { it.isWhitespace() }
|
||||
|
||||
// Construct final TOTP URI
|
||||
"$OTPAUTH_PREFIX$encodedLabel?secret=$sanitizedSecret$issuerParameter"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import android.os.Build
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
|
||||
/**
|
||||
@@ -7,3 +8,55 @@ import com.x8bit.bitwarden.BuildConfig
|
||||
*/
|
||||
val isFdroid: Boolean
|
||||
get() = BuildConfig.FLAVOR == "fdroid"
|
||||
|
||||
/**
|
||||
* A string that represents a displayable app version.
|
||||
*/
|
||||
val versionData: String
|
||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||
|
||||
/**
|
||||
* A string that represents device data.
|
||||
*/
|
||||
val deviceData: String get() = "$deviceBrandModel $osInfo $buildInfo"
|
||||
|
||||
/**
|
||||
* A string representing the CI information if available.
|
||||
*/
|
||||
val ciBuildInfo: String? get() = BuildConfig.CI_INFO.takeUnless { it.isBlank() }
|
||||
|
||||
/**
|
||||
* A string representing the build flavor or blank if it is the standard configuration.
|
||||
*/
|
||||
private val buildFlavorName: String
|
||||
get() = when (BuildConfig.FLAVOR) {
|
||||
"standard" -> ""
|
||||
else -> "-${BuildConfig.FLAVOR}"
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the build type.
|
||||
*/
|
||||
private val buildTypeName: String
|
||||
get() = when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> "dev"
|
||||
"release" -> "prod"
|
||||
else -> BuildConfig.BUILD_TYPE
|
||||
}
|
||||
|
||||
/**
|
||||
* A string representing the device brand and model.
|
||||
*/
|
||||
private val deviceBrandModel: String get() = "\uD83D\uDCF1 ${Build.BRAND} ${Build.MODEL}"
|
||||
|
||||
/**
|
||||
* A string representing the operating system information.
|
||||
*/
|
||||
private val osInfo: String get() = "\uD83E\uDD16 ${Build.VERSION.RELEASE}@${Build.VERSION.SDK_INT}"
|
||||
|
||||
/**
|
||||
* A string representing the build information.
|
||||
*/
|
||||
private val buildInfo: String
|
||||
get() = "\uD83D\uDCE6 $buildTypeName" +
|
||||
buildFlavorName.takeUnless { it.isBlank() }?.let { " $it" }.orEmpty()
|
||||
|
||||
@@ -130,7 +130,7 @@ class GeneratorRepositoryImpl(
|
||||
}
|
||||
GeneratedPasswordResult.Success(generatedPassword)
|
||||
},
|
||||
onFailure = { GeneratedPasswordResult.InvalidRequest },
|
||||
onFailure = { GeneratedPasswordResult.InvalidRequest(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun generatePassphrase(
|
||||
@@ -149,7 +149,7 @@ class GeneratorRepositoryImpl(
|
||||
}
|
||||
GeneratedPassphraseResult.Success(generatedPassphrase)
|
||||
},
|
||||
onFailure = { GeneratedPassphraseResult.InvalidRequest },
|
||||
onFailure = { GeneratedPassphraseResult.InvalidRequest(error = it) },
|
||||
)
|
||||
|
||||
override suspend fun generatePlusAddressedEmail(
|
||||
@@ -161,7 +161,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedPlusAddressedUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedPlusAddressedUsernameResult.InvalidRequest
|
||||
GeneratedPlusAddressedUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -174,7 +174,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedCatchAllUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedCatchAllUsernameResult.InvalidRequest
|
||||
GeneratedCatchAllUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -187,7 +187,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedRandomWordUsernameResult.Success(generatedUsername)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedRandomWordUsernameResult.InvalidRequest
|
||||
GeneratedRandomWordUsernameResult.InvalidRequest(error = it)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ class GeneratorRepositoryImpl(
|
||||
GeneratedForwardedServiceUsernameResult.Success(generatedEmail)
|
||||
},
|
||||
onFailure = {
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message)
|
||||
GeneratedForwardedServiceUsernameResult.InvalidRequest(it.message, error = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ sealed class GeneratedCatchAllUsernameResult {
|
||||
/**
|
||||
* There was an error during the operation.
|
||||
*/
|
||||
data object InvalidRequest : GeneratedCatchAllUsernameResult()
|
||||
data class InvalidRequest(val error: Throwable) : GeneratedCatchAllUsernameResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,8 @@ sealed class GeneratedForwardedServiceUsernameResult {
|
||||
/**
|
||||
* There was an error during the operation.
|
||||
*/
|
||||
data class InvalidRequest(val message: String?) : GeneratedForwardedServiceUsernameResult()
|
||||
data class InvalidRequest(
|
||||
val message: String?,
|
||||
val error: Throwable,
|
||||
) : GeneratedForwardedServiceUsernameResult()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -15,5 +15,6 @@ sealed class InitializeCryptoResult {
|
||||
*/
|
||||
data class AuthenticationError(
|
||||
val message: String? = null,
|
||||
val error: Throwable,
|
||||
) : InitializeCryptoResult()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,5 +14,7 @@ sealed class DownloadResult {
|
||||
/**
|
||||
* The download failed.
|
||||
*/
|
||||
data object Failure : DownloadResult()
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : DownloadResult()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -17,5 +17,5 @@ sealed class CreateAttachmentResult {
|
||||
/**
|
||||
* Generic error while creating an attachment.
|
||||
*/
|
||||
data object Error : CreateAttachmentResult()
|
||||
data class Error(val error: Throwable) : CreateAttachmentResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class CreateCipherResult {
|
||||
/**
|
||||
* Generic error while creating cipher.
|
||||
*/
|
||||
data object Error : CreateCipherResult()
|
||||
data class Error(val error: Throwable) : CreateCipherResult()
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ sealed class CreateFolderResult {
|
||||
/**
|
||||
* Generic error while creating a folder.
|
||||
*/
|
||||
data object Error : CreateFolderResult()
|
||||
data class Error(val error: Throwable) : CreateFolderResult()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ sealed class DecryptFido2CredentialAutofillViewResult {
|
||||
/**
|
||||
* Generic error while decrypting credentials.
|
||||
*/
|
||||
data object Error : DecryptFido2CredentialAutofillViewResult()
|
||||
data class Error(val error: Throwable) : DecryptFido2CredentialAutofillViewResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class DeleteAttachmentResult {
|
||||
/**
|
||||
* Generic error while deleting an attachment.
|
||||
*/
|
||||
data object Error : DeleteAttachmentResult()
|
||||
data class Error(val error: Throwable) : DeleteAttachmentResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class DeleteCipherResult {
|
||||
/**
|
||||
* Generic error while deleting a cipher.
|
||||
*/
|
||||
data object Error : DeleteCipherResult()
|
||||
data class Error(val error: Throwable) : DeleteCipherResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class DeleteFolderResult {
|
||||
/**
|
||||
* Generic error while deleting a folder.
|
||||
*/
|
||||
data object Error : DeleteFolderResult()
|
||||
data class Error(val error: Throwable) : DeleteFolderResult()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class DeleteSendResult {
|
||||
/**
|
||||
* Generic error while deleting a send.
|
||||
*/
|
||||
data object Error : DeleteSendResult()
|
||||
data class Error(val error: Throwable) : DeleteSendResult()
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@ sealed class DownloadAttachmentResult {
|
||||
/**
|
||||
* The attachment could not be downloaded.
|
||||
*/
|
||||
data object Failure : DownloadAttachmentResult()
|
||||
data class Failure(val error: Throwable) : DownloadAttachmentResult()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -13,5 +13,5 @@ sealed class RestoreCipherResult {
|
||||
/**
|
||||
* Generic error while restoring a cipher.
|
||||
*/
|
||||
data object Error : RestoreCipherResult()
|
||||
data class Error(val error: Throwable) : RestoreCipherResult()
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ sealed class ShareCipherResult {
|
||||
/**
|
||||
* Generic error while sharing cipher.
|
||||
*/
|
||||
data object Error : ShareCipherResult()
|
||||
data class Error(val error: Throwable) : ShareCipherResult()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user