Compare commits

...

5 Commits

Author SHA1 Message Date
David Perez
277fcbf14c
🍒 Add Biometric logging (#5645) 2025-08-05 12:22:52 -05:00
Patrick Honkonen
729ec60ba8
🍒[PM-24206] Fix filtered verification code search (#5622) 2025-07-30 17:18:30 +00:00
Patrick Honkonen
3d220cf765
🍒 [PM-24205] Fix Fido2CredentialStore to save new credentials correctly (#5604) 2025-07-28 20:35:06 +00:00
Patrick Honkonen
df2acadea0
🍒 [PM-24204] Correct TOTP generation to use cipherId instead of totpCode (#5603) 2025-07-28 20:32:35 +00:00
David Perez
7043b4be26
🍒 Fix crash in Android 13 (#5591) 2025-07-25 14:31:24 -05:00
26 changed files with 108 additions and 121 deletions

View File

@ -24,11 +24,12 @@ class AutofillTotpManagerImpl(
if (settingsRepository.isAutoCopyTotpDisabled) return if (settingsRepository.isAutoCopyTotpDisabled) return
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
if (!isPremium && !cipherView.organizationUseTotp) return if (!isPremium && !cipherView.organizationUseTotp) return
val totpCode = cipherView.login?.totp ?: return cipherView.login?.totp ?: return
val cipherId = cipherView.id ?: return
val totpResult = vaultRepository.generateTotp( val totpResult = vaultRepository.generateTotp(
time = clock.instant(), time = clock.instant(),
totpCode = totpCode, cipherId = cipherId,
) )
if (totpResult is GenerateTotpResult.Success) { if (totpResult is GenerateTotpResult.Success) {

View File

@ -7,6 +7,7 @@ import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.BuildConfig import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import timber.log.Timber
import java.security.InvalidAlgorithmParameterException import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException import java.security.InvalidKeyException
import java.security.KeyStore import java.security.KeyStore
@ -45,9 +46,11 @@ class BiometricsEncryptionManagerImpl(
} }
val cipher = try { val cipher = try {
Cipher.getInstance(CIPHER_TRANSFORMATION) Cipher.getInstance(CIPHER_TRANSFORMATION)
} catch (_: NoSuchAlgorithmException) { } catch (nsae: NoSuchAlgorithmException) {
Timber.w(nsae, "createCipherOrNull failed to get cipher instance")
return null return null
} catch (_: NoSuchPaddingException) { } catch (nspe: NoSuchPaddingException) {
Timber.w(nspe, "createCipherOrNull failed to get cipher instance")
return null return null
} }
// Instantiate integrity values. // Instantiate integrity values.
@ -124,20 +127,25 @@ class BiometricsEncryptionManagerImpl(
private fun generateKeyOrNull(userId: String): SecretKey? { private fun generateKeyOrNull(userId: String): SecretKey? {
val keyGen = try { val keyGen = try {
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME) KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ENCRYPTION_KEYSTORE_NAME)
} catch (_: NoSuchAlgorithmException) { } catch (nsae: NoSuchAlgorithmException) {
Timber.w(nsae, "generateKeyOrNull failed to get key generator instance")
return null return null
} catch (_: NoSuchProviderException) { } catch (nspe: NoSuchProviderException) {
Timber.w(nspe, "generateKeyOrNull failed to get key generator instance")
return null return null
} catch (_: IllegalArgumentException) { } catch (iae: IllegalArgumentException) {
Timber.w(iae, "generateKeyOrNull failed to get key generator instance")
return null return null
} }
return try { return try {
keyGen.init(getKeyGenParameterSpec(userId = userId)) keyGen.init(getKeyGenParameterSpec(userId = userId))
keyGen.generateKey() keyGen.generateKey()
} catch (_: InvalidAlgorithmParameterException) { } catch (iape: InvalidAlgorithmParameterException) {
Timber.w(iape, "generateKeyOrNull failed to initialize and generate key")
null null
} catch (_: ProviderException) { } catch (pe: ProviderException) {
Timber.w(pe, "generateKeyOrNull failed to initialize and generate key")
null null
} }
} }
@ -150,14 +158,17 @@ class BiometricsEncryptionManagerImpl(
keystore keystore
.getKey(encryptionKeyName(userId = userId), null) .getKey(encryptionKeyName(userId = userId), null)
?.let { it as SecretKey } ?.let { it as SecretKey }
} catch (_: KeyStoreException) { } catch (kse: KeyStoreException) {
// keystore was not loaded // keystore was not loaded
Timber.w(kse, "getSecretKeyOrNull failed to retrieve secret key")
null null
} catch (_: NoSuchAlgorithmException) { } catch (nsae: NoSuchAlgorithmException) {
// keystore algorithm cannot be found // keystore algorithm cannot be found
Timber.w(nsae, "getSecretKeyOrNull failed to retrieve secret key")
null null
} catch (_: UnrecoverableKeyException) { } catch (uke: UnrecoverableKeyException) {
// key could not be recovered // key could not be recovered
Timber.w(uke, "getSecretKeyOrNull failed to retrieve secret key")
null null
} }
@ -174,16 +185,19 @@ class BiometricsEncryptionManagerImpl(
?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) } ?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) }
?: init(Cipher.ENCRYPT_MODE, secretKey) ?: init(Cipher.ENCRYPT_MODE, secretKey)
true true
} catch (_: KeyPermanentlyInvalidatedException) { } catch (kpie: KeyPermanentlyInvalidatedException) {
// Biometric has changed // Biometric has changed
Timber.w(kpie, "initializeCipher failed to initialize cipher")
destroyBiometrics(userId = userId) destroyBiometrics(userId = userId)
false false
} catch (_: UnrecoverableKeyException) { } catch (uke: UnrecoverableKeyException) {
// Biometric was disabled and re-enabled // Biometric was disabled and re-enabled
Timber.w(uke, "initializeCipher failed to initialize cipher")
destroyBiometrics(userId = userId) destroyBiometrics(userId = userId)
false false
} catch (_: InvalidKeyException) { } catch (ike: InvalidKeyException) {
// User has no key // User has no key
Timber.w(ike, "initializeCipher failed to initialize cipher")
destroyBiometrics(userId = userId) destroyBiometrics(userId = userId)
true true
} }

View File

@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
import java.security.GeneralSecurityException
import java.time.Instant import java.time.Instant
import javax.crypto.Cipher import javax.crypto.Cipher
@ -501,9 +503,14 @@ class SettingsRepositoryImpl(
.onSuccess { biometricsKey -> .onSuccess { biometricsKey ->
authDiskSource.storeUserBiometricUnlockKey( authDiskSource.storeUserBiometricUnlockKey(
userId = userId, userId = userId,
biometricsKey = cipher biometricsKey = try {
.doFinal(biometricsKey.encodeToByteArray()) cipher
.toString(Charsets.ISO_8859_1), .doFinal(biometricsKey.encodeToByteArray())
.toString(Charsets.ISO_8859_1)
} catch (e: GeneralSecurityException) {
Timber.w(e, "setupBiometricsKey failed encrypt the biometric key")
return BiometricsKeyResult.Error(error = e)
},
) )
authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv) authDiskSource.storeUserBiometricInitVector(userId = userId, iv = cipher.iv)
} }

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.DateTime
import com.bitwarden.core.DerivePinKeyResponse import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitOrgCryptoRequest
import com.bitwarden.core.InitUserCryptoMethod import com.bitwarden.core.InitUserCryptoMethod
@ -373,15 +372,6 @@ interface VaultSdkSource {
passwordHistoryList: List<PasswordHistory>, passwordHistoryList: List<PasswordHistory>,
): Result<List<PasswordHistoryView>> ): Result<List<PasswordHistoryView>>
/**
* Generate a verification code and the period using the totp code.
*/
suspend fun generateTotp(
userId: String,
totp: String,
time: DateTime,
): Result<TotpResponse>
/** /**
* Generate a verification code for the given [cipherListView] and [time]. * Generate a verification code for the given [cipherListView] and [time].
*/ */

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.data.vault.datasource.sdk package com.x8bit.bitwarden.data.vault.datasource.sdk
import com.bitwarden.core.DateTime
import com.bitwarden.core.DeriveKeyConnectorRequest import com.bitwarden.core.DeriveKeyConnectorRequest
import com.bitwarden.core.DerivePinKeyResponse import com.bitwarden.core.DerivePinKeyResponse
import com.bitwarden.core.InitOrgCryptoRequest import com.bitwarden.core.InitOrgCryptoRequest
@ -417,19 +416,6 @@ class VaultSdkSourceImpl(
.decryptList(list = passwordHistoryList) .decryptList(list = passwordHistoryList)
} }
override suspend fun generateTotp(
userId: String,
totp: String,
time: DateTime,
): Result<TotpResponse> = runCatchingWithLogs {
getClient(userId = userId)
.vault()
.generateTotp(
key = totp,
time = time,
)
}
override suspend fun generateTotpForCipherListView( override suspend fun generateTotpForCipherListView(
userId: String, userId: String,
cipherListView: CipherListView, cipherListView: CipherListView,

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
import com.bitwarden.data.manager.DispatcherManager import com.bitwarden.data.manager.DispatcherManager
import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.sdk.Fido2CredentialStore
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
@ -50,8 +51,12 @@ object VaultSdkModule {
@Provides @Provides
@Singleton @Singleton
fun providesFido2CredentialStore( fun providesFido2CredentialStore(
authRepository: AuthRepository,
vaultSdkSource: VaultSdkSource,
vaultRepository: VaultRepository, vaultRepository: VaultRepository,
): Fido2CredentialStore = Fido2CredentialStoreImpl( ): Fido2CredentialStore = Fido2CredentialStoreImpl(
authRepository = authRepository,
vaultSdkSource = vaultSdkSource,
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
) )
} }

View File

@ -5,8 +5,11 @@ import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.vault.CipherListView import com.bitwarden.vault.CipherListView
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.bitwarden.vault.EncryptionContext import com.bitwarden.vault.EncryptionContext
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.autofill.util.login import com.x8bit.bitwarden.data.autofill.util.login
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult import com.x8bit.bitwarden.data.vault.manager.model.GetCipherResult
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
@ -17,6 +20,8 @@ import timber.log.Timber
*/ */
@OmitFromCoverage @OmitFromCoverage
class Fido2CredentialStoreImpl( class Fido2CredentialStoreImpl(
private val authRepository: AuthRepository,
private val vaultSdkSource: VaultSdkSource,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
) : Fido2CredentialStore { ) : Fido2CredentialStore {
@ -79,18 +84,17 @@ class Fido2CredentialStoreImpl(
* Save the provided [cred] to the users vault. * Save the provided [cred] to the users vault.
*/ */
override suspend fun saveCredential(cred: EncryptionContext) { override suspend fun saveCredential(cred: EncryptionContext) {
vaultRepository.getCipher(cred.cipher.id.orEmpty()) vaultSdkSource
.toCipherViewOrNull() .decryptCipher(
?.let { cipherView -> userId = authRepository.activeUserId ?: throw NoActiveUserException(),
cipherView.id cipher = cred.cipher,
?.let { cipherId -> )
vaultRepository.updateCipher( .map { decryptedCipherView ->
cipherId = cipherId, decryptedCipherView.id
cipherView = cipherView, ?.let { vaultRepository.updateCipher(it, decryptedCipherView) }
) ?: vaultRepository.createCipher(decryptedCipherView)
}
?: vaultRepository.createCipher(cipherView = cipherView)
} }
.onFailure { throw it }
} }
/** /**

View File

@ -225,7 +225,7 @@ interface VaultRepository : CipherManager, VaultLockManager {
/** /**
* Attempt to get the verification code and the period. * Attempt to get the verification code and the period.
*/ */
suspend fun generateTotp(totpCode: String, time: DateTime): GenerateTotpResult suspend fun generateTotp(cipherId: String, time: DateTime): GenerateTotpResult
/** /**
* Attempt to delete a send. * Attempt to delete a send.

View File

@ -121,6 +121,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.time.Clock import java.time.Clock
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -572,6 +573,7 @@ class VaultRepositoryImpl(
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1)) .doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
.decodeToString() .decodeToString()
} catch (e: GeneralSecurityException) { } catch (e: GeneralSecurityException) {
Timber.w(e, "unlockVaultWithBiometrics failed when decrypting biometrics key")
return VaultUnlockResult.BiometricDecodingError(error = e) return VaultUnlockResult.BiometricDecodingError(error = e)
} }
} }
@ -585,6 +587,7 @@ class VaultRepositoryImpl(
.doFinal(biometricsKey.encodeToByteArray()) .doFinal(biometricsKey.encodeToByteArray())
.toString(Charsets.ISO_8859_1) .toString(Charsets.ISO_8859_1)
} catch (e: GeneralSecurityException) { } catch (e: GeneralSecurityException) {
Timber.w(e, "unlockVaultWithBiometrics failed to migrate the user to IV encryption")
return VaultUnlockResult.BiometricDecodingError(error = e) return VaultUnlockResult.BiometricDecodingError(error = e)
} }
} else { } else {
@ -802,15 +805,24 @@ class VaultRepositoryImpl(
} }
override suspend fun generateTotp( override suspend fun generateTotp(
totpCode: String, cipherId: String,
time: DateTime, time: DateTime,
): GenerateTotpResult { ): GenerateTotpResult {
val userId = activeUserId val userId = activeUserId
?: return GenerateTotpResult.Error(error = NoActiveUserException()) ?: return GenerateTotpResult.Error(error = NoActiveUserException())
return vaultSdkSource.generateTotp( val cipherListView = decryptCipherListResultStateFlow
.value
.data
?.successes
?.find { it.id == cipherId }
?: return GenerateTotpResult.Error(
error = IllegalArgumentException(cipherId),
)
return vaultSdkSource.generateTotpForCipherListView(
time = time, time = time,
userId = userId, userId = userId,
totp = totpCode, cipherListView = cipherListView,
) )
.fold( .fold(
onSuccess = { onSuccess = {

View File

@ -414,7 +414,7 @@ class SearchViewModel @Inject constructor(
action: ListingItemOverflowAction.VaultAction.CopyTotpClick, action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val result = vaultRepo.generateTotp(action.totpCode, clock.instant()) val result = vaultRepo.generateTotp(action.cipherId, clock.instant())
sendAction(SearchAction.Internal.GenerateTotpResultReceive(result)) sendAction(SearchAction.Internal.GenerateTotpResultReceive(result))
} }
} }

View File

@ -125,7 +125,7 @@ private fun CipherListView.filterBySearchType(
} }
is SearchTypeData.Vault.SshKeys -> type is CipherListViewType.SshKey && deletedDate == null is SearchTypeData.Vault.SshKeys -> type is CipherListViewType.SshKey && deletedDate == null
is SearchTypeData.Vault.VerificationCodes -> organizationUseTotp && deletedDate == null is SearchTypeData.Vault.VerificationCodes -> login?.totp != null && deletedDate == null
is SearchTypeData.Vault.Trash -> deletedDate != null is SearchTypeData.Vault.Trash -> deletedDate != null
} }

View File

@ -1239,7 +1239,7 @@ class VaultItemListingViewModel @Inject constructor(
action: ListingItemOverflowAction.VaultAction.CopyTotpClick, action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val result = vaultRepository.generateTotp(action.totpCode, clock.instant()) val result = vaultRepository.generateTotp(action.cipherId, clock.instant())
sendAction(VaultItemListingsAction.Internal.GenerateTotpResultReceive(result)) sendAction(VaultItemListingsAction.Internal.GenerateTotpResultReceive(result))
} }
} }

View File

@ -136,7 +136,7 @@ sealed class ListingItemOverflowAction : Parcelable {
*/ */
@Parcelize @Parcelize
data class CopyTotpClick( data class CopyTotpClick(
val totpCode: String, val cipherId: String,
override val requiresPasswordReprompt: Boolean, override val requiresPasswordReprompt: Boolean,
) : VaultAction() { ) : VaultAction() {
override val title: Text get() = BitwardenString.copy_totp.asText() override val title: Text get() = BitwardenString.copy_totp.asText()

View File

@ -39,7 +39,7 @@ fun CipherListView.toOverflowActions(
this.login?.totp this.login?.totp
?.let { ?.let {
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = it, cipherId = cipherId,
requiresPasswordReprompt = hasMasterPassword, requiresPasswordReprompt = hasMasterPassword,
) )
} }

View File

@ -634,7 +634,7 @@ class VaultViewModel @Inject constructor(
action: ListingItemOverflowAction.VaultAction.CopyTotpClick, action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val result = vaultRepository.generateTotp(action.totpCode, clock.instant()) val result = vaultRepository.generateTotp(action.cipherId, clock.instant())
sendAction(VaultAction.Internal.GenerateTotpResultReceive(result)) sendAction(VaultAction.Internal.GenerateTotpResultReceive(result))
} }
} }

View File

@ -128,7 +128,7 @@ class AutofillTotpManagerTest {
} }
every { loginView.totp } returns TOTP_CODE every { loginView.totp } returns TOTP_CODE
coEvery { coEvery {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE) vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), cipherId = "cipherId")
} returns generateTotpResult } returns generateTotpResult
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView) autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
@ -141,7 +141,7 @@ class AutofillTotpManagerTest {
settingsRepository.isAutoCopyTotpDisabled settingsRepository.isAutoCopyTotpDisabled
} }
coVerify(exactly = 1) { coVerify(exactly = 1) {
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE) vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), cipherId = "cipherId")
} }
} }
} }

View File

@ -69,9 +69,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.security.MessageDigest import java.security.MessageDigest
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultSdkSourceTest { class VaultSdkSourceTest {
@ -977,30 +974,6 @@ class VaultSdkSourceTest {
coVerify { sdkClientManager.getOrCreateClient(userId = userId) } coVerify { sdkClientManager.getOrCreateClient(userId = userId) }
} }
@Test
fun `generateTotp should call SDK and return a Result with correct data`() = runTest {
val userId = "userId"
val totpResponse = TotpResponse("TestCode", 30u)
coEvery { clientVault.generateTotp(any(), any()) } returns totpResponse
val time = FIXED_CLOCK.instant()
val result = vaultSdkSource.generateTotp(
userId = userId,
totp = "Totp",
time = time,
)
assertEquals(totpResponse.asSuccess(), result)
coVerify {
clientVault.generateTotp(
key = "Totp",
time = time,
)
}
coVerify { sdkClientManager.getOrCreateClient(userId = userId) }
}
@Test @Test
fun `generateTotpForCipherListView should call SDK and return a Result with correct data`() = fun `generateTotpForCipherListView should call SDK and return a Result with correct data`() =
runTest { runTest {
@ -1422,7 +1395,3 @@ private val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
isUserVerificationSupported = true, isUserVerificationSupported = true,
selectedCipherView = createMockCipherView(number = 1), selectedCipherView = createMockCipherView(number = 1),
) )
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)

View File

@ -143,10 +143,10 @@ class TotpCodeManagerTest {
runTest { runTest {
val totpResponse = TotpResponse("123456", 30u) val totpResponse = TotpResponse("123456", 30u)
coEvery { coEvery {
vaultSdkSource.generateTotp( vaultSdkSource.generateTotpForCipherListView(
userId = any(), userId = any(),
totp = any(),
time = any(), time = any(),
cipherListView = any(),
) )
} returns totpResponse.asSuccess() } returns totpResponse.asSuccess()

View File

@ -2740,7 +2740,7 @@ class VaultRepositoryTest {
fakeAuthDiskSource.userState = null fakeAuthDiskSource.userState = null
val result = vaultRepository.generateTotp( val result = vaultRepository.generateTotp(
totpCode = "totpCode", cipherId = "totpCode",
time = DateTime.now(), time = DateTime.now(),
) )
@ -2753,13 +2753,16 @@ class VaultRepositoryTest {
@Test @Test
fun `generateTotp should return a success result on getting a code`() = runTest { fun `generateTotp should return a success result on getting a code`() = runTest {
val totpResponse = TotpResponse("Testcode", 30u) val totpResponse = TotpResponse("Testcode", 30u)
val userId = "mockId-1"
coEvery { coEvery {
vaultSdkSource.generateTotp(any(), any(), any()) vaultSdkSource.generateTotpForCipherListView(any(), any(), any())
} returns totpResponse.asSuccess() } returns totpResponse.asSuccess()
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
setVaultToUnlocked(userId = userId)
setupDataStateFlow(userId = userId)
val result = vaultRepository.generateTotp( val result = vaultRepository.generateTotp(
totpCode = "testCode", cipherId = "mockId-1",
time = DateTime.now(), time = DateTime.now(),
) )

View File

@ -1007,7 +1007,7 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
SearchAction.OverflowOptionClick( SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),
@ -1035,7 +1035,7 @@ class SearchViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
SearchAction.OverflowOptionClick( SearchAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),

View File

@ -52,7 +52,7 @@ fun createMockDisplayItemForCipher(
cipherId = "mockId-$number", cipherId = "mockId-$number",
), ),
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = "mockTotp-$number", cipherId = "mockId-$number",
requiresPasswordReprompt = true, requiresPasswordReprompt = true,
), ),
ListingItemOverflowAction.VaultAction.ViewClick( ListingItemOverflowAction.VaultAction.ViewClick(

View File

@ -1954,7 +1954,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.OverflowOptionClick( VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),
@ -1982,7 +1982,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.OverflowOptionClick( VaultItemListingsAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),

View File

@ -62,7 +62,7 @@ fun createMockDisplayItemForCipher(
cipherId = "mockId-$number", cipherId = "mockId-$number",
), ),
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = "mockTotp-$number", cipherId = "mockId-$number",
requiresPasswordReprompt = requiresPasswordReprompt, requiresPasswordReprompt = requiresPasswordReprompt,
), ),
ListingItemOverflowAction.VaultAction.ViewClick( ListingItemOverflowAction.VaultAction.ViewClick(

View File

@ -45,7 +45,7 @@ class CipherListViewExtensionsTest {
cipherId = id, cipherId = id,
), ),
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = id,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
ListingItemOverflowAction.VaultAction.ViewClick( ListingItemOverflowAction.VaultAction.ViewClick(

View File

@ -1931,7 +1931,7 @@ class VaultViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
VaultAction.OverflowOptionClick( VaultAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),
@ -1959,7 +1959,7 @@ class VaultViewModelTest : BaseViewModelTest() {
viewModel.trySendAction( viewModel.trySendAction(
VaultAction.OverflowOptionClick( VaultAction.OverflowOptionClick(
ListingItemOverflowAction.VaultAction.CopyTotpClick( ListingItemOverflowAction.VaultAction.CopyTotpClick(
totpCode = totpCode, cipherId = totpCode,
requiresPasswordReprompt = false, requiresPasswordReprompt = false,
), ),
), ),

View File

@ -1,9 +1,9 @@
package com.bitwarden.ui.platform.util package com.bitwarden.ui.platform.util
import android.os.Build
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.util.Base64 import android.util.Base64
import androidx.core.os.ParcelCompat
import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
@ -105,7 +105,7 @@ open class ParcelableRouteSerializer<T : Parcelable>(
} }
} }
encodedString encodedString
?.toParcelable<T>() ?.toParcelable()
?: throw IllegalStateException("Invalid decoding for ${kClass.qualifiedName}.") ?: throw IllegalStateException("Invalid decoding for ${kClass.qualifiedName}.")
} }
@ -137,15 +137,11 @@ open class ParcelableRouteSerializer<T : Parcelable>(
} }
val value = try { val value = try {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { ParcelCompat.readParcelable(
parcel.readParcelable( parcel,
ParcelableRouteSerializer::class.java.classLoader, ParcelableRouteSerializer::class.java.classLoader,
kClass.java, kClass.java,
) ) as T?
} else {
@Suppress("DEPRECATION")
parcel.readParcelable(ParcelableRouteSerializer::class.java.classLoader)
} as T?
} catch (_: IllegalArgumentException) { } catch (_: IllegalArgumentException) {
null null
} catch (_: IllegalStateException) { } catch (_: IllegalStateException) {