mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 08:35:05 -06:00
Compare commits
5 Commits
main
...
v2025.7.2-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
277fcbf14c | ||
|
|
729ec60ba8 | ||
|
|
3d220cf765 | ||
|
|
df2acadea0 | ||
|
|
7043b4be26 |
@ -24,11 +24,12 @@ class AutofillTotpManagerImpl(
|
||||
if (settingsRepository.isAutoCopyTotpDisabled) return
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
if (!isPremium && !cipherView.organizationUseTotp) return
|
||||
val totpCode = cipherView.login?.totp ?: return
|
||||
cipherView.login?.totp ?: return
|
||||
val cipherId = cipherView.id ?: return
|
||||
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = clock.instant(),
|
||||
totpCode = totpCode,
|
||||
cipherId = cipherId,
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
|
||||
@ -7,6 +7,7 @@ import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.BuildConfig
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import timber.log.Timber
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.KeyStore
|
||||
@ -45,9 +46,11 @@ class BiometricsEncryptionManagerImpl(
|
||||
}
|
||||
val cipher = try {
|
||||
Cipher.getInstance(CIPHER_TRANSFORMATION)
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (nsae: NoSuchAlgorithmException) {
|
||||
Timber.w(nsae, "createCipherOrNull failed to get cipher instance")
|
||||
return null
|
||||
} catch (_: NoSuchPaddingException) {
|
||||
} catch (nspe: NoSuchPaddingException) {
|
||||
Timber.w(nspe, "createCipherOrNull failed to get cipher instance")
|
||||
return null
|
||||
}
|
||||
// Instantiate integrity values.
|
||||
@ -124,20 +127,25 @@ class BiometricsEncryptionManagerImpl(
|
||||
private fun generateKeyOrNull(userId: String): SecretKey? {
|
||||
val keyGen = try {
|
||||
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
|
||||
} catch (_: NoSuchProviderException) {
|
||||
} catch (nspe: NoSuchProviderException) {
|
||||
Timber.w(nspe, "generateKeyOrNull failed to get key generator instance")
|
||||
return null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (iae: IllegalArgumentException) {
|
||||
Timber.w(iae, "generateKeyOrNull failed to get key generator instance")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
keyGen.init(getKeyGenParameterSpec(userId = userId))
|
||||
keyGen.generateKey()
|
||||
} catch (_: InvalidAlgorithmParameterException) {
|
||||
} catch (iape: InvalidAlgorithmParameterException) {
|
||||
Timber.w(iape, "generateKeyOrNull failed to initialize and generate key")
|
||||
null
|
||||
} catch (_: ProviderException) {
|
||||
} catch (pe: ProviderException) {
|
||||
Timber.w(pe, "generateKeyOrNull failed to initialize and generate key")
|
||||
null
|
||||
}
|
||||
}
|
||||
@ -150,14 +158,17 @@ class BiometricsEncryptionManagerImpl(
|
||||
keystore
|
||||
.getKey(encryptionKeyName(userId = userId), null)
|
||||
?.let { it as SecretKey }
|
||||
} catch (_: KeyStoreException) {
|
||||
} catch (kse: KeyStoreException) {
|
||||
// keystore was not loaded
|
||||
Timber.w(kse, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (nsae: NoSuchAlgorithmException) {
|
||||
// keystore algorithm cannot be found
|
||||
Timber.w(nsae, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
} catch (uke: UnrecoverableKeyException) {
|
||||
// key could not be recovered
|
||||
Timber.w(uke, "getSecretKeyOrNull failed to retrieve secret key")
|
||||
null
|
||||
}
|
||||
|
||||
@ -174,16 +185,19 @@ class BiometricsEncryptionManagerImpl(
|
||||
?.let { init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(it)) }
|
||||
?: init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
true
|
||||
} catch (_: KeyPermanentlyInvalidatedException) {
|
||||
} catch (kpie: KeyPermanentlyInvalidatedException) {
|
||||
// Biometric has changed
|
||||
Timber.w(kpie, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
false
|
||||
} catch (_: UnrecoverableKeyException) {
|
||||
} catch (uke: UnrecoverableKeyException) {
|
||||
// Biometric was disabled and re-enabled
|
||||
Timber.w(uke, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
false
|
||||
} catch (_: InvalidKeyException) {
|
||||
} catch (ike: InvalidKeyException) {
|
||||
// User has no key
|
||||
Timber.w(ike, "initializeCipher failed to initialize cipher")
|
||||
destroyBiometrics(userId = userId)
|
||||
true
|
||||
}
|
||||
|
||||
@ -35,6 +35,8 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.security.GeneralSecurityException
|
||||
import java.time.Instant
|
||||
import javax.crypto.Cipher
|
||||
|
||||
@ -501,9 +503,14 @@ class SettingsRepositoryImpl(
|
||||
.onSuccess { biometricsKey ->
|
||||
authDiskSource.storeUserBiometricUnlockKey(
|
||||
userId = userId,
|
||||
biometricsKey = cipher
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1),
|
||||
biometricsKey = try {
|
||||
cipher
|
||||
.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)
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
@ -373,15 +372,6 @@ interface VaultSdkSource {
|
||||
passwordHistoryList: List<PasswordHistory>,
|
||||
): 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].
|
||||
*/
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.sdk
|
||||
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.DeriveKeyConnectorRequest
|
||||
import com.bitwarden.core.DerivePinKeyResponse
|
||||
import com.bitwarden.core.InitOrgCryptoRequest
|
||||
@ -417,19 +416,6 @@ class VaultSdkSourceImpl(
|
||||
.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(
|
||||
userId: String,
|
||||
cipherListView: CipherListView,
|
||||
|
||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.vault.datasource.sdk.di
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
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.SdkClientManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.sdk.SdkRepositoryFactory
|
||||
@ -50,8 +51,12 @@ object VaultSdkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFido2CredentialStore(
|
||||
authRepository: AuthRepository,
|
||||
vaultSdkSource: VaultSdkSource,
|
||||
vaultRepository: VaultRepository,
|
||||
): Fido2CredentialStore = Fido2CredentialStoreImpl(
|
||||
authRepository = authRepository,
|
||||
vaultSdkSource = vaultSdkSource,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,8 +5,11 @@ import com.bitwarden.sdk.Fido2CredentialStore
|
||||
import com.bitwarden.vault.CipherListView
|
||||
import com.bitwarden.vault.CipherView
|
||||
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.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.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
|
||||
@ -17,6 +20,8 @@ import timber.log.Timber
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
class Fido2CredentialStoreImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultSdkSource: VaultSdkSource,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : Fido2CredentialStore {
|
||||
|
||||
@ -79,18 +84,17 @@ class Fido2CredentialStoreImpl(
|
||||
* Save the provided [cred] to the users vault.
|
||||
*/
|
||||
override suspend fun saveCredential(cred: EncryptionContext) {
|
||||
vaultRepository.getCipher(cred.cipher.id.orEmpty())
|
||||
.toCipherViewOrNull()
|
||||
?.let { cipherView ->
|
||||
cipherView.id
|
||||
?.let { cipherId ->
|
||||
vaultRepository.updateCipher(
|
||||
cipherId = cipherId,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
}
|
||||
?: vaultRepository.createCipher(cipherView = cipherView)
|
||||
vaultSdkSource
|
||||
.decryptCipher(
|
||||
userId = authRepository.activeUserId ?: throw NoActiveUserException(),
|
||||
cipher = cred.cipher,
|
||||
)
|
||||
.map { decryptedCipherView ->
|
||||
decryptedCipherView.id
|
||||
?.let { vaultRepository.updateCipher(it, decryptedCipherView) }
|
||||
?: vaultRepository.createCipher(decryptedCipherView)
|
||||
}
|
||||
.onFailure { throw it }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -225,7 +225,7 @@ interface VaultRepository : CipherManager, VaultLockManager {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@ -121,6 +121,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.security.GeneralSecurityException
|
||||
import java.time.Clock
|
||||
import java.time.temporal.ChronoUnit
|
||||
@ -572,6 +573,7 @@ class VaultRepositoryImpl(
|
||||
.doFinal(biometricsKey.toByteArray(Charsets.ISO_8859_1))
|
||||
.decodeToString()
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Timber.w(e, "unlockVaultWithBiometrics failed when decrypting biometrics key")
|
||||
return VaultUnlockResult.BiometricDecodingError(error = e)
|
||||
}
|
||||
}
|
||||
@ -585,6 +587,7 @@ class VaultRepositoryImpl(
|
||||
.doFinal(biometricsKey.encodeToByteArray())
|
||||
.toString(Charsets.ISO_8859_1)
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Timber.w(e, "unlockVaultWithBiometrics failed to migrate the user to IV encryption")
|
||||
return VaultUnlockResult.BiometricDecodingError(error = e)
|
||||
}
|
||||
} else {
|
||||
@ -802,15 +805,24 @@ class VaultRepositoryImpl(
|
||||
}
|
||||
|
||||
override suspend fun generateTotp(
|
||||
totpCode: String,
|
||||
cipherId: String,
|
||||
time: DateTime,
|
||||
): GenerateTotpResult {
|
||||
val userId = activeUserId
|
||||
?: 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,
|
||||
userId = userId,
|
||||
totp = totpCode,
|
||||
cipherListView = cipherListView,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = {
|
||||
|
||||
@ -414,7 +414,7 @@ class SearchViewModel @Inject constructor(
|
||||
action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepo.generateTotp(action.totpCode, clock.instant())
|
||||
val result = vaultRepo.generateTotp(action.cipherId, clock.instant())
|
||||
sendAction(SearchAction.Internal.GenerateTotpResultReceive(result))
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ private fun CipherListView.filterBySearchType(
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -1239,7 +1239,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository.generateTotp(action.totpCode, clock.instant())
|
||||
val result = vaultRepository.generateTotp(action.cipherId, clock.instant())
|
||||
sendAction(VaultItemListingsAction.Internal.GenerateTotpResultReceive(result))
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ sealed class ListingItemOverflowAction : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data class CopyTotpClick(
|
||||
val totpCode: String,
|
||||
val cipherId: String,
|
||||
override val requiresPasswordReprompt: Boolean,
|
||||
) : VaultAction() {
|
||||
override val title: Text get() = BitwardenString.copy_totp.asText()
|
||||
|
||||
@ -39,7 +39,7 @@ fun CipherListView.toOverflowActions(
|
||||
this.login?.totp
|
||||
?.let {
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = it,
|
||||
cipherId = cipherId,
|
||||
requiresPasswordReprompt = hasMasterPassword,
|
||||
)
|
||||
}
|
||||
|
||||
@ -634,7 +634,7 @@ class VaultViewModel @Inject constructor(
|
||||
action: ListingItemOverflowAction.VaultAction.CopyTotpClick,
|
||||
) {
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository.generateTotp(action.totpCode, clock.instant())
|
||||
val result = vaultRepository.generateTotp(action.cipherId, clock.instant())
|
||||
sendAction(VaultAction.Internal.GenerateTotpResultReceive(result))
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@ class AutofillTotpManagerTest {
|
||||
}
|
||||
every { loginView.totp } returns TOTP_CODE
|
||||
coEvery {
|
||||
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
|
||||
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), cipherId = "cipherId")
|
||||
} returns generateTotpResult
|
||||
|
||||
autofillTotpManager.tryCopyTotpToClipboard(cipherView = cipherView)
|
||||
@ -141,7 +141,7 @@ class AutofillTotpManagerTest {
|
||||
settingsRepository.isAutoCopyTotpDisabled
|
||||
}
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), totpCode = TOTP_CODE)
|
||||
vaultRepository.generateTotp(time = FIXED_CLOCK.instant(), cipherId = "cipherId")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,9 +69,6 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.security.MessageDigest
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultSdkSourceTest {
|
||||
@ -977,30 +974,6 @@ class VaultSdkSourceTest {
|
||||
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
|
||||
fun `generateTotpForCipherListView should call SDK and return a Result with correct data`() =
|
||||
runTest {
|
||||
@ -1422,7 +1395,3 @@ private val DEFAULT_FIDO_2_AUTH_REQUEST = AuthenticateFido2CredentialRequest(
|
||||
isUserVerificationSupported = true,
|
||||
selectedCipherView = createMockCipherView(number = 1),
|
||||
)
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
@ -143,10 +143,10 @@ class TotpCodeManagerTest {
|
||||
runTest {
|
||||
val totpResponse = TotpResponse("123456", 30u)
|
||||
coEvery {
|
||||
vaultSdkSource.generateTotp(
|
||||
vaultSdkSource.generateTotpForCipherListView(
|
||||
userId = any(),
|
||||
totp = any(),
|
||||
time = any(),
|
||||
cipherListView = any(),
|
||||
)
|
||||
} returns totpResponse.asSuccess()
|
||||
|
||||
|
||||
@ -2740,7 +2740,7 @@ class VaultRepositoryTest {
|
||||
fakeAuthDiskSource.userState = null
|
||||
|
||||
val result = vaultRepository.generateTotp(
|
||||
totpCode = "totpCode",
|
||||
cipherId = "totpCode",
|
||||
time = DateTime.now(),
|
||||
)
|
||||
|
||||
@ -2753,13 +2753,16 @@ class VaultRepositoryTest {
|
||||
@Test
|
||||
fun `generateTotp should return a success result on getting a code`() = runTest {
|
||||
val totpResponse = TotpResponse("Testcode", 30u)
|
||||
val userId = "mockId-1"
|
||||
coEvery {
|
||||
vaultSdkSource.generateTotp(any(), any(), any())
|
||||
vaultSdkSource.generateTotpForCipherListView(any(), any(), any())
|
||||
} returns totpResponse.asSuccess()
|
||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||
setVaultToUnlocked(userId = userId)
|
||||
setupDataStateFlow(userId = userId)
|
||||
|
||||
val result = vaultRepository.generateTotp(
|
||||
totpCode = "testCode",
|
||||
cipherId = "mockId-1",
|
||||
time = DateTime.now(),
|
||||
)
|
||||
|
||||
|
||||
@ -1007,7 +1007,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
@ -1035,7 +1035,7 @@ class SearchViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
SearchAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -52,7 +52,7 @@ fun createMockDisplayItemForCipher(
|
||||
cipherId = "mockId-$number",
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = "mockTotp-$number",
|
||||
cipherId = "mockId-$number",
|
||||
requiresPasswordReprompt = true,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
|
||||
@ -1954,7 +1954,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
@ -1982,7 +1982,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -62,7 +62,7 @@ fun createMockDisplayItemForCipher(
|
||||
cipherId = "mockId-$number",
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = "mockTotp-$number",
|
||||
cipherId = "mockId-$number",
|
||||
requiresPasswordReprompt = requiresPasswordReprompt,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
|
||||
@ -45,7 +45,7 @@ class CipherListViewExtensionsTest {
|
||||
cipherId = id,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = id,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
ListingItemOverflowAction.VaultAction.ViewClick(
|
||||
|
||||
@ -1931,7 +1931,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
@ -1959,7 +1959,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
VaultAction.OverflowOptionClick(
|
||||
ListingItemOverflowAction.VaultAction.CopyTotpClick(
|
||||
totpCode = totpCode,
|
||||
cipherId = totpCode,
|
||||
requiresPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package com.bitwarden.ui.platform.util
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Base64
|
||||
import androidx.core.os.ParcelCompat
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
@ -105,7 +105,7 @@ open class ParcelableRouteSerializer<T : Parcelable>(
|
||||
}
|
||||
}
|
||||
encodedString
|
||||
?.toParcelable<T>()
|
||||
?.toParcelable()
|
||||
?: throw IllegalStateException("Invalid decoding for ${kClass.qualifiedName}.")
|
||||
}
|
||||
|
||||
@ -137,15 +137,11 @@ open class ParcelableRouteSerializer<T : Parcelable>(
|
||||
}
|
||||
val value = try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
parcel.readParcelable(
|
||||
ParcelableRouteSerializer::class.java.classLoader,
|
||||
kClass.java,
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
parcel.readParcelable(ParcelableRouteSerializer::class.java.classLoader)
|
||||
} as T?
|
||||
ParcelCompat.readParcelable(
|
||||
parcel,
|
||||
ParcelableRouteSerializer::class.java.classLoader,
|
||||
kClass.java,
|
||||
) as T?
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
} catch (_: IllegalStateException) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user