Repository: bitwarden/android
Pull request #4893: PM-19334: Propagate errors to the UI
Author: login: david-livefront
Created: 2025-03-18T16:06:44Z | Merged: 2025-03-18T17:53:42Z

## 🎟️ Tracking

[PM-19334](https://bitwarden.atlassian.net/browse/PM-19334)

## 📔 Objective

This PR propagates the remain non-generator errors to the UI.

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes


[PM-19334]: https://bitwarden.atlassian.net/browse/PM-19334?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerImpl.kt
index ef23908bdd8..e8f031dfb61 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerImpl.kt
@@ -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)
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/ImportPrivateKeyResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/ImportPrivateKeyResult.kt
index 2cf360ddb68..1df61bdbe3f 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/ImportPrivateKeyResult.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/ImportPrivateKeyResult.kt
@@ -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()
     }
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt
index 13364b92694..239d9edebd7 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt
@@ -494,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 ->
@@ -508,7 +509,7 @@ class SettingsRepositoryImpl(
             }
             .fold(
                 onSuccess = { BiometricsKeyResult.Success },
-                onFailure = { BiometricsKeyResult.Error },
+                onFailure = { BiometricsKeyResult.Error(error = it) },
             )
     }
 
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/BiometricsKeyResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/BiometricsKeyResult.kt
index 029b8a7ac21..0d41d422101 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/BiometricsKeyResult.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/model/BiometricsKeyResult.kt
@@ -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()
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/FindFido2CredentialsResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/FindFido2CredentialsResult.kt
deleted file mode 100644
index 8dc1618ae0b..00000000000
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/FindFido2CredentialsResult.kt
+++ /dev/null
@@ -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()
-}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt
deleted file mode 100644
index 7788cbf574e..00000000000
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt
+++ /dev/null
@@ -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()
-}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt
index 4f2d247a26d..f7ac707d790 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerImpl.kt
@@ -446,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
         }
 
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt
index c8332784775..57fcb0716ea 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/FileManagerImpl.kt
@@ -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
         }
     }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/DownloadResult.kt b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/DownloadResult.kt
index 5bbc4ed8125..fb96897f46b 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/DownloadResult.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/vault/manager/model/DownloadResult.kt
@@ -14,5 +14,7 @@ sealed class DownloadResult {
     /**
      * The download failed.
      */
-    data object Failure : DownloadResult()
+    data class Failure(
+        val error: Throwable,
+    ) : DownloadResult()
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt
index a0a39e29ea0..7611fdd2a61 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt
@@ -164,7 +164,7 @@ class SetupUnlockViewModel @Inject constructor(
                 settingsRepository.storeUnlockPin(
                     pin = state.pin,
                     shouldRequireMasterPasswordOnRestart =
-                    state.shouldRequireMasterPasswordOnRestart,
+                        state.shouldRequireMasterPasswordOnRestart,
                 )
             }
         }
@@ -182,7 +182,7 @@ class SetupUnlockViewModel @Inject constructor(
         action: SetupUnlockAction.Internal.BiometricsKeyResultReceive,
     ) {
         when (action.result) {
-            BiometricsKeyResult.Error -> {
+            is BiometricsKeyResult.Error -> {
                 mutableStateFlow.update {
                     it.copy(
                         dialogState = null,
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt
index 8deaf6be257..4059e24efa9 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt
@@ -74,8 +74,8 @@ class AccountSecurityViewModel @Inject constructor(
                 ?.hasMasterPassword != false,
             isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
             shouldShowEnableAuthenticatorSync =
-            featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) &&
-                !isBuildVersionBelow(Build.VERSION_CODES.S),
+                featureFlagManager.getFeatureFlag(FlagKey.AuthenticatorSync) &&
+                    !isBuildVersionBelow(Build.VERSION_CODES.S),
             userId = userId,
             vaultTimeout = settingsRepository.vaultTimeout,
             vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
@@ -365,7 +365,7 @@ class AccountSecurityViewModel @Inject constructor(
                 settingsRepository.storeUnlockPin(
                     pin = state.pin,
                     shouldRequireMasterPasswordOnRestart =
-                    state.shouldRequireMasterPasswordOnRestart,
+                        state.shouldRequireMasterPasswordOnRestart,
                 )
             }
         }
@@ -464,7 +464,7 @@ class AccountSecurityViewModel @Inject constructor(
         action: AccountSecurityAction.Internal.BiometricsKeyResultReceive,
     ) {
         when (action.result) {
-            BiometricsKeyResult.Error -> {
+            is BiometricsKeyResult.Error -> {
                 mutableStateFlow.update {
                     it.copy(
                         dialog = null,
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
index b529c94d055..80fa7d44924 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt
@@ -316,7 +316,7 @@ class AuthRepositoryTest {
             GetTokenResponseJson.Success::toUserState,
             UserStateJson::toRemovedPasswordUserStateJson,
         )
-        unmockkStatic(
+        mockkConstructor(
             NoActiveUserException::class,
             MissingPropertyException::class,
         )
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerTest.kt
index 3c70f1a47b9..179fa549386 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/KeyManagerTest.kt
@@ -3,14 +3,17 @@ package com.x8bit.bitwarden.data.platform.manager
 import android.content.Context
 import android.security.KeyChain
 import android.security.KeyChainException
-import com.x8bit.bitwarden.data.platform.manager.model.ImportPrivateKeyResult
 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 io.mockk.every
 import io.mockk.just
 import io.mockk.mockk
+import io.mockk.mockkConstructor
 import io.mockk.mockkStatic
 import io.mockk.runs
+import io.mockk.unmockkConstructor
 import io.mockk.unmockkStatic
 import io.mockk.verify
 import org.junit.jupiter.api.AfterEach
@@ -39,11 +42,16 @@ class KeyManagerTest {
     @BeforeEach
     fun setUp() {
         mockkStatic(KeyStore::class, KeyChain::class)
+        mockkConstructor(MissingPropertyException::class)
+        every {
+            anyConstructed<MissingPropertyException>() == any<MissingPropertyException>()
+        } returns true
     }
 
     @AfterEach
     fun tearDown() {
         unmockkStatic(KeyStore::class, KeyChain::class)
+        unmockkConstructor(MissingPropertyException::class)
     }
 
     @Test
@@ -367,11 +375,10 @@ class KeyManagerTest {
         val password = "password"
 
         // Verify KeyStoreException is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws KeyStoreException()
+        val keystoreError = KeyStoreException()
+        every { mockPkcs12KeyStore.load(any(), any()) } throws keystoreError
         assertEquals(
-            ImportPrivateKeyResult.Error.UnsupportedKey,
+            ImportPrivateKeyResult.Error.UnsupportedKey(throwable = keystoreError),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -380,11 +387,12 @@ class KeyManagerTest {
         ) { "KeyStoreException was not handled correctly" }
 
         // Verify IOException is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws IOException()
+        val keystoreOperationIoError = IOException()
+        every { mockPkcs12KeyStore.load(any(), any()) } throws keystoreOperationIoError
         assertEquals(
-            ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
+            ImportPrivateKeyResult.Error.KeyStoreOperationFailed(
+                throwable = keystoreOperationIoError,
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -393,12 +401,10 @@ class KeyManagerTest {
         ) { "IOException was not handled correctly" }
 
         // Verify IOException with UnrecoverableKeyException cause is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws IOException(UnrecoverableKeyException())
-
+        val unrecoverableKeyError = IOException(UnrecoverableKeyException())
+        every { mockPkcs12KeyStore.load(any(), any()) } throws unrecoverableKeyError
         assertEquals(
-            ImportPrivateKeyResult.Error.UnrecoverableKey,
+            ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = unrecoverableKeyError),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -407,11 +413,12 @@ class KeyManagerTest {
         )
 
         // Verify IOException with unexpected cause is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws IOException(Exception())
+        val keystoreOperationError = IOException(Exception())
+        every { mockPkcs12KeyStore.load(any(), any()) } throws keystoreOperationError
         assertEquals(
-            ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
+            ImportPrivateKeyResult.Error.KeyStoreOperationFailed(
+                throwable = keystoreOperationError,
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -420,11 +427,10 @@ class KeyManagerTest {
         ) { "IOException with Unexpected exception cause was not handled correctly" }
 
         // Verify CertificateException is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws CertificateException()
+        val certificateError = CertificateException()
+        every { mockPkcs12KeyStore.load(any(), any()) } throws certificateError
         assertEquals(
-            ImportPrivateKeyResult.Error.InvalidCertificateChain,
+            ImportPrivateKeyResult.Error.InvalidCertificateChain(throwable = certificateError),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -433,11 +439,10 @@ class KeyManagerTest {
         ) { "CertificateException was not handled correctly" }
 
         // Verify NoSuchAlgorithmException is handled
-        every {
-            mockPkcs12KeyStore.load(any(), any())
-        } throws NoSuchAlgorithmException()
+        val algorithmError = NoSuchAlgorithmException()
+        every { mockPkcs12KeyStore.load(any(), any()) } throws algorithmError
         assertEquals(
-            ImportPrivateKeyResult.Error.UnsupportedKey,
+            ImportPrivateKeyResult.Error.UnsupportedKey(throwable = algorithmError),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -458,7 +463,9 @@ class KeyManagerTest {
         }
 
         assertEquals(
-            ImportPrivateKeyResult.Error.UnsupportedKey,
+            ImportPrivateKeyResult.Error.UnsupportedKey(
+                throwable = MissingPropertyException("Internal Alias"),
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -481,15 +488,16 @@ class KeyManagerTest {
             every { hasMoreElements() } returns true
             every { nextElement() } returns "mockInternalAlias"
         }
+        val error = UnrecoverableKeyException()
         every {
             mockPkcs12KeyStore.getKey(
                 "mockInternalAlias",
                 password.toCharArray(),
             )
-        } throws UnrecoverableKeyException()
+        } throws error
 
         assertEquals(
-            ImportPrivateKeyResult.Error.UnrecoverableKey,
+            ImportPrivateKeyResult.Error.UnrecoverableKey(throwable = error),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -504,7 +512,9 @@ class KeyManagerTest {
             )
         } returns null
         assertEquals(
-            ImportPrivateKeyResult.Error.UnrecoverableKey,
+            ImportPrivateKeyResult.Error.UnrecoverableKey(
+                throwable = MissingPropertyException("Private Key"),
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -537,7 +547,9 @@ class KeyManagerTest {
             mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
         } returns emptyArray()
         assertEquals(
-            ImportPrivateKeyResult.Error.InvalidCertificateChain,
+            ImportPrivateKeyResult.Error.InvalidCertificateChain(
+                throwable = MissingPropertyException("Certificate Chain"),
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -550,7 +562,9 @@ class KeyManagerTest {
             mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
         } returns null
         assertEquals(
-            ImportPrivateKeyResult.Error.InvalidCertificateChain,
+            ImportPrivateKeyResult.Error.InvalidCertificateChain(
+                throwable = MissingPropertyException("Certificate Chain"),
+            ),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
@@ -563,9 +577,11 @@ class KeyManagerTest {
     @Test
     fun `importMutualTlsCertificate should return KeyStoreOperationFailed when saving to Android KeyStore throws KeyStoreException`() {
         setupMockAndroidKeyStore()
+        setupMockPkcs12KeyStore()
         val expectedAlias = "mockAlias"
         val pkcs12Bytes = "key.p12".toByteArray()
         val password = "password"
+        val error = KeyStoreException()
 
         every { mockPkcs12KeyStore.aliases() } returns mockk {
             every { hasMoreElements() } returns true
@@ -582,6 +598,8 @@ class KeyManagerTest {
             mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
         } returns arrayOf(mockk())
 
+        every { mockAndroidKeyStore.containsAlias(expectedAlias) } returns false
+
         every {
             mockAndroidKeyStore.setKeyEntry(
                 expectedAlias,
@@ -589,10 +607,10 @@ class KeyManagerTest {
                 any(),
                 any(),
             )
-        } throws KeyStoreException()
+        } throws error
 
         assertEquals(
-            ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
+            ImportPrivateKeyResult.Error.KeyStoreOperationFailed(throwable = error),
             keyDiskSource.importMutualTlsCertificate(
                 key = pkcs12Bytes,
                 alias = expectedAlias,
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt
index 14e9eca5f94..c6e5269c2f8 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt
@@ -833,7 +833,7 @@ class SettingsRepositoryTest {
 
             val result = settingsRepository.setupBiometricsKey(cipher = cipher)
 
-            assertEquals(BiometricsKeyResult.Error, result)
+            assertEquals(BiometricsKeyResult.Error(error = NoActiveUserException()), result)
             coVerify(exactly = 0) {
                 vaultSdkSource.getUserEncryptionKey(userId = any())
             }
@@ -845,13 +845,14 @@ class SettingsRepositoryTest {
         runTest {
             fakeAuthDiskSource.userState = MOCK_USER_STATE
             val cipher = mockk<Cipher>()
+            val error = Throwable("Fail")
             coEvery {
                 vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
-            } returns Throwable("Fail").asFailure()
+            } returns error.asFailure()
 
             val result = settingsRepository.setupBiometricsKey(cipher = cipher)
 
-            assertEquals(BiometricsKeyResult.Error, result)
+            assertEquals(BiometricsKeyResult.Error(error = error), result)
             coVerify(exactly = 1) {
                 vaultSdkSource.getUserEncryptionKey(userId = USER_ID)
             }
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt
index 6f3293df760..a3c124eec34 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/manager/CipherManagerTest.kt
@@ -1857,10 +1857,9 @@ class CipherManagerTest {
         coEvery {
             ciphersService.getCipherAttachment(cipherId = any(), attachmentId = any())
         } returns response.asSuccess()
-
         coEvery {
             fileManager.downloadFileToCache(url = any())
-        } returns DownloadResult.Failure
+        } returns DownloadResult.Failure(error = Throwable("Fail!"))
 
         assertEquals(
             DownloadAttachmentResult.Failure(IllegalStateException()),
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt
index aa6e65689ca..95be41185d6 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt
@@ -169,7 +169,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() {
         runTest {
             coEvery {
                 settingsRepository.setupBiometricsKey(CIPHER)
-            } returns BiometricsKeyResult.Error
+            } returns BiometricsKeyResult.Error(error = Throwable("Fail!"))
             val viewModel = createViewModel()
 
             viewModel.stateFlow.test {
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModelTest.kt
index 4cde15cfe92..c5d94f1aacf 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentViewModelTest.kt
@@ -519,7 +519,9 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
         viewModel.eventFlow.test {
             viewModel.trySendAction(
                 EnvironmentAction.Internal.ImportKeyResultReceive(
-                    result = ImportPrivateKeyResult.Error.UnsupportedKey,
+                    result = ImportPrivateKeyResult.Error.UnsupportedKey(
+                        throwable = Throwable("Fail!"),
+                    ),
                 ),
             )
             assertEquals(
@@ -529,7 +531,9 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
 
             viewModel.trySendAction(
                 EnvironmentAction.Internal.ImportKeyResultReceive(
-                    result = ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
+                    result = ImportPrivateKeyResult.Error.KeyStoreOperationFailed(
+                        throwable = Throwable("Fail!"),
+                    ),
                 ),
             )
             assertEquals(
@@ -539,7 +543,9 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
 
             viewModel.trySendAction(
                 EnvironmentAction.Internal.ImportKeyResultReceive(
-                    result = ImportPrivateKeyResult.Error.UnrecoverableKey,
+                    result = ImportPrivateKeyResult.Error.UnrecoverableKey(
+                        throwable = Throwable("Fail!"),
+                    ),
                 ),
             )
             assertEquals(
@@ -549,7 +555,9 @@ class EnvironmentViewModelTest : BaseViewModelTest() {
 
             viewModel.trySendAction(
                 EnvironmentAction.Internal.ImportKeyResultReceive(
-                    result = ImportPrivateKeyResult.Error.InvalidCertificateChain,
+                    result = ImportPrivateKeyResult.Error.InvalidCertificateChain(
+                        throwable = Throwable("Fail!"),
+                    ),
                 ),
             )
             assertEquals(
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt
index 340d1b4a534..9b467010bbc 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt
@@ -589,7 +589,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
         runTest {
             coEvery {
                 settingsRepository.setupBiometricsKey(cipher = CIPHER)
-            } returns BiometricsKeyResult.Error
+            } returns BiometricsKeyResult.Error(error = Throwable("Fail!"))
             val viewModel = createViewModel()
 
             viewModel.stateFlow.test {


Reviews:
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T16:07:49Z
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T16:07:54Z
user: login: dseverns-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T17:06:49Z
user: login: dseverns-livefront
body: Just want to confirm about removing those FIDO 2 things if they are related to some blocked work or not otherwise 👍 


state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-18T17:07:55Z
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T17:27:36Z

Review comments:
diff_hunk: @@ -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()
-}
path: app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/FindFido2CredentialsResult.kt
user: login: david-livefront
body: This class is never referenced, so I am removing it.
created_at: 2025-03-18T16:07:48Z
author_association: COLLABORATOR
diff_hunk: @@ -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 {
path: app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt
user: login: david-livefront
body: This class is never referenced, so I am removing it.
created_at: 2025-03-18T16:07:54Z
author_association: COLLABORATOR
diff_hunk: @@ -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 {
path: app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt
user: login: dseverns-livefront
body: @patrick-bitwarden is there anything that is to be done that will use this?
created_at: 2025-03-18T17:06:49Z
author_association: COLLABORATOR
diff_hunk: @@ -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 {
path: app/src/main/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/SaveCredentialResult.kt
user: login: david-livefront
body: I spoke to him ahead of time, we should be good here 😄 
created_at: 2025-03-18T17:27:36Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-18T16:08:49Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/android/pull/4893?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.16%. Comparing base [(`3934bc9`)](https://app.codecov.io/gh/bitwarden/android/commit/3934bc9ae247c9e32a81de92e508d6659b59c870?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`2f5b354`)](https://app.codecov.io/gh/bitwarden/android/commit/2f5b354026ec9f5d42265f44e035075766c4ec66?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 4 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #4893      +/-   ##
==========================================
- Coverage   89.48%   84.16%   -5.33%     
==========================================
  Files         499      618     +119     
  Lines       42488    48213    +5725     
  Branches     5985     6669     +684     
==========================================
+ Hits        38020    40577    +2557     
- Misses       2401     5387    +2986     
- Partials     2067     2249     +182     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/android/pull/4893?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-18T16:12:35Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [ace737c6\-4609\-4896\-82ab\-1dc9a678b882](https://ast.checkmarx.net/projects/7b48c130-9c07-4ac2-a6dd-15ed4933d967/scans?id=ace737c6-4609-4896-82ab-1dc9a678b882&branch=PM-19334-remaining-errors)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: david-livefront
created_at: 2025-03-18T17:27:18Z
body: Thanks @dseverns-livefront 
author_association: COLLABORATOR