mirror of
https://github.com/bitwarden/android.git
synced 2025-01-30 19:53:47 +03:00
Merge branch 'main' into PM-13626/remember-last-opened-view
# Conflicts: # app/src/main/java/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt
This commit is contained in:
commit
c18b5184c4
22 changed files with 1224 additions and 110 deletions
app/src
main
java/com/x8bit/bitwarden
data/platform
datasource/disk/model
manager
ui
auth/feature
landing
login
startregistration
twofactorlogin
platform/components
res
test/java/com/x8bit/bitwarden
data/platform/manager
ui/auth/feature
|
@ -0,0 +1,45 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
/**
|
||||
* Models the result of importing a private key.
|
||||
*/
|
||||
sealed class ImportPrivateKeyResult {
|
||||
|
||||
/**
|
||||
* Represents a successful result of importing a private key.
|
||||
*
|
||||
* @property alias The alias assigned to the imported private key.
|
||||
*/
|
||||
data class Success(val alias: String) : ImportPrivateKeyResult()
|
||||
|
||||
/**
|
||||
* Represents a generic error during the import process.
|
||||
*/
|
||||
sealed class Error : ImportPrivateKeyResult() {
|
||||
|
||||
/**
|
||||
* Indicates that the provided key is unrecoverable or the password is incorrect.
|
||||
*/
|
||||
data object UnrecoverableKey : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the certificate chain associated with the key is invalid.
|
||||
*/
|
||||
data object InvalidCertificateChain : Error()
|
||||
|
||||
/**
|
||||
* Indicates that the specified alias is already in use.
|
||||
*/
|
||||
data object DuplicateAlias : Error()
|
||||
|
||||
/**
|
||||
* Indicates that an error occurred during the key store operation.
|
||||
*/
|
||||
data object KeyStoreOperationFailed : Error()
|
||||
|
||||
/**
|
||||
* Indicates the provided key is not supported.
|
||||
*/
|
||||
data object UnsupportedKey : Error()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Represents a mutual TLS certificate.
|
||||
*/
|
||||
data class MutualTlsCertificate(
|
||||
val alias: String,
|
||||
val privateKey: PrivateKey,
|
||||
val certificateChain: List<X509Certificate>,
|
||||
) {
|
||||
/**
|
||||
* Leaf certificate of the chain.
|
||||
*/
|
||||
val leafCertificate: X509Certificate?
|
||||
get() = certificateChain.lastOrNull()
|
||||
|
||||
/**
|
||||
* Root certificate of the chain.
|
||||
*/
|
||||
val rootCertificate: X509Certificate?
|
||||
get() = certificateChain.firstOrNull()
|
||||
|
||||
override fun toString(): String = leafCertificate
|
||||
?.let {
|
||||
buildString {
|
||||
appendLine("Subject: ${it.subjectDN}")
|
||||
appendLine("Issuer: ${it.issuerDN}")
|
||||
appendLine("Valid From: ${it.notBefore}")
|
||||
appendLine("Valid Until: ${it.notAfter}")
|
||||
}
|
||||
}
|
||||
?: ""
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.disk.model
|
||||
|
||||
/**
|
||||
* Location of the key data.
|
||||
*/
|
||||
enum class MutualTlsKeyHost {
|
||||
/**
|
||||
* Key is stored in the system key chain.
|
||||
*/
|
||||
KEY_CHAIN,
|
||||
|
||||
/**
|
||||
* Key is stored in a private instance of the Android Key Store.
|
||||
*/
|
||||
ANDROID_KEY_STORE,
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.x8bit.bitwarden.data.platform.manager
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
|
||||
/**
|
||||
* Primary access point for disk information related to key data.
|
||||
*/
|
||||
interface KeyManager {
|
||||
|
||||
/**
|
||||
* Import a private key into the application KeyStore.
|
||||
*
|
||||
* @param key The private key to be saved.
|
||||
* @param alias Alias to be assigned to the private key.
|
||||
* @param password Password used to protect the certificate.
|
||||
*/
|
||||
fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
password: String,
|
||||
): ImportPrivateKeyResult
|
||||
|
||||
/**
|
||||
* Removes the mTLS key from storage.
|
||||
*/
|
||||
fun removeMutualTlsKey(alias: String, host: MutualTlsKeyHost)
|
||||
|
||||
/**
|
||||
* Retrieve the certificate chain for the selected mTLS key.
|
||||
*/
|
||||
fun getMutualTlsCertificateChain(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
): MutualTlsCertificate?
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
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.datasource.disk.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.PrivateKey
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* Default implementation of [KeyManager].
|
||||
*/
|
||||
class KeyManagerImpl(
|
||||
private val context: Context,
|
||||
) : KeyManager {
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
override fun importMutualTlsCertificate(
|
||||
key: ByteArray,
|
||||
alias: String,
|
||||
password: String,
|
||||
): ImportPrivateKeyResult {
|
||||
// Step 1: Load PKCS12 bytes into a KeyStore.
|
||||
val pkcs12KeyStore: KeyStore = key
|
||||
.inputStream()
|
||||
.use { stream ->
|
||||
try {
|
||||
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
|
||||
} catch (e: IOException) {
|
||||
Timber.Forest.e(e, "Format or password error while loading PKCS12 bytes")
|
||||
return when (e.cause) {
|
||||
is UnrecoverableKeyException -> {
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
}
|
||||
|
||||
else -> {
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed
|
||||
}
|
||||
}
|
||||
} catch (e: CertificateException) {
|
||||
Timber.Forest.e(e, "Unable to load certificate chain")
|
||||
return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Cryptographic algorithm not supported")
|
||||
return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Get a list of aliases and choose the first one.
|
||||
val internalAlias = pkcs12KeyStore.aliases()
|
||||
?.takeIf { it.hasMoreElements() }
|
||||
?.nextElement()
|
||||
?: return ImportPrivateKeyResult.Error.UnsupportedKey
|
||||
|
||||
// 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
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to get private key")
|
||||
return ImportPrivateKeyResult.Error.UnrecoverableKey
|
||||
}
|
||||
|
||||
val certChain: Array<Certificate> = pkcs12KeyStore
|
||||
.getCertificateChain(internalAlias)
|
||||
?.takeUnless { it.isEmpty() }
|
||||
?: return ImportPrivateKeyResult.Error.InvalidCertificateChain
|
||||
|
||||
// Step 4: Store the private key and X.509 certificate in the AndroidKeyStore if the alias
|
||||
// does not exists.
|
||||
with(androidKeyStore) {
|
||||
if (containsAlias(alias)) {
|
||||
return ImportPrivateKeyResult.Error.DuplicateAlias
|
||||
}
|
||||
|
||||
try {
|
||||
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.Success(alias)
|
||||
}
|
||||
|
||||
override fun removeMutualTlsKey(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
) {
|
||||
when (host) {
|
||||
MutualTlsKeyHost.ANDROID_KEY_STORE -> removeKeyFromAndroidKeyStore(alias)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMutualTlsCertificateChain(
|
||||
alias: String,
|
||||
host: MutualTlsKeyHost,
|
||||
): MutualTlsCertificate? = when (host) {
|
||||
MutualTlsKeyHost.ANDROID_KEY_STORE -> getKeyFromAndroidKeyStore(alias)
|
||||
|
||||
MutualTlsKeyHost.KEY_CHAIN -> getSystemKeySpecOrNull(alias)
|
||||
}
|
||||
|
||||
private fun removeKeyFromAndroidKeyStore(alias: String) {
|
||||
try {
|
||||
androidKeyStore.deleteEntry(alias)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to remove key from Android KeyStore")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSystemKeySpecOrNull(alias: String): MutualTlsCertificate? {
|
||||
val systemPrivateKey = try {
|
||||
KeyChain.getPrivateKey(context, alias)
|
||||
} catch (e: KeyChainException) {
|
||||
Timber.Forest.e(e, "Requested alias not found in system KeyChain")
|
||||
null
|
||||
}
|
||||
?: return null
|
||||
|
||||
val systemCertificateChain = try {
|
||||
KeyChain.getCertificateChain(context, alias)
|
||||
} catch (e: KeyChainException) {
|
||||
Timber.Forest.e(e, "Unable to access certificate chain for provided alias")
|
||||
null
|
||||
}
|
||||
?: return null
|
||||
|
||||
return MutualTlsCertificate(
|
||||
alias = alias,
|
||||
certificateChain = systemCertificateChain.toList(),
|
||||
privateKey = systemPrivateKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getKeyFromAndroidKeyStore(alias: String): MutualTlsCertificate? =
|
||||
with(androidKeyStore) {
|
||||
try {
|
||||
val privateKeyRef = (getKey(alias, null) as? PrivateKey)
|
||||
?: return null
|
||||
val certChain = getCertificateChain(alias)
|
||||
.mapNotNull { it as? X509Certificate }
|
||||
.takeUnless { it.isEmpty() }
|
||||
?: return null
|
||||
MutualTlsCertificate(
|
||||
alias = alias,
|
||||
certificateChain = certChain,
|
||||
privateKey = privateKeyRef,
|
||||
)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.Forest.e(e, "Failed to load Android KeyStore")
|
||||
null
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
Timber.Forest.e(e, "Failed to load client certificate from Android KeyStore")
|
||||
null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Key cannot be recovered. Password may be incorrect.")
|
||||
null
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
Timber.Forest.e(e, "Algorithm not supported")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val androidKeyStore
|
||||
get() = KeyStore
|
||||
.getInstance(KEYSTORE_TYPE_ANDROID)
|
||||
.also { it.load(null) }
|
||||
}
|
||||
|
||||
private const val KEYSTORE_TYPE_ANDROID = "AndroidKeyStore"
|
||||
private const val KEYSTORE_TYPE_PKCS12 = "pkcs12"
|
|
@ -30,6 +30,8 @@ import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
|||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.KeyManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.LogsManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
|
@ -333,6 +335,12 @@ object PlatformManagerModule {
|
|||
accessibilityEnabledManager = accessibilityEnabledManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideKeyManager(
|
||||
@ApplicationContext context: Context,
|
||||
): KeyManager = KeyManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppResumeManager(
|
||||
|
|
|
@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
@ -121,18 +121,18 @@ fun LandingScreen(
|
|||
null -> Unit
|
||||
}
|
||||
|
||||
val isAppBarVisible = state.accountSummaries.isNotEmpty()
|
||||
var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
|
||||
state = rememberTopAppBarState(),
|
||||
canScroll = { !isAccountMenuVisible },
|
||||
)
|
||||
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
if (isAppBarVisible) {
|
||||
if (state.isAppBarVisible) {
|
||||
BitwardenTopAppBar(
|
||||
title = "",
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
@ -170,7 +170,6 @@ fun LandingScreen(
|
|||
) {
|
||||
LandingScreenContent(
|
||||
state = state,
|
||||
isAppBarVisible = isAppBarVisible,
|
||||
onEmailInputChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LandingAction.EmailInputChanged(it)) }
|
||||
},
|
||||
|
@ -195,7 +194,6 @@ fun LandingScreen(
|
|||
@Composable
|
||||
private fun LandingScreenContent(
|
||||
state: LandingState,
|
||||
isAppBarVisible: Boolean,
|
||||
onEmailInputChange: (String) -> Unit,
|
||||
onEnvironmentTypeSelect: (Environment.Type) -> Unit,
|
||||
onRememberMeToggle: (Boolean) -> Unit,
|
||||
|
@ -207,28 +205,26 @@ private fun LandingScreenContent(
|
|||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
.verticalScroll(rememberScrollState())
|
||||
.statusBarsPadding(),
|
||||
) {
|
||||
val topPadding = if (isAppBarVisible) 40.dp else 104.dp
|
||||
Spacer(modifier = Modifier.height(topPadding))
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.logo),
|
||||
painter = rememberVectorPainter(id = R.drawable.bitwarden_logo),
|
||||
colorFilter = ColorFilter.tint(BitwardenTheme.colorScheme.icon.secondary),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.width(220.dp)
|
||||
.height(74.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.login_or_create_new_account),
|
||||
text = stringResource(id = R.string.login_to_bitwarden),
|
||||
textAlign = TextAlign.Center,
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
|
@ -237,9 +233,7 @@ private fun LandingScreenContent(
|
|||
.wrapContentHeight(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
modifier = Modifier
|
||||
|
@ -253,7 +247,8 @@ private fun LandingScreenContent(
|
|||
cardStyle = CardStyle.Full,
|
||||
supportingTextContent = {
|
||||
EnvironmentSelector(
|
||||
labelText = stringResource(id = R.string.logging_in_on),
|
||||
labelText = stringResource(id = R.string.logging_in_on_with_colon),
|
||||
dialogTitle = stringResource(id = R.string.logging_in_on),
|
||||
selectedOption = state.selectedEnvironmentType,
|
||||
onOptionSelected = onEnvironmentTypeSelect,
|
||||
modifier = Modifier
|
||||
|
@ -266,8 +261,8 @@ private fun LandingScreenContent(
|
|||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.remember_me),
|
||||
isChecked = state.isRememberMeEnabled,
|
||||
label = stringResource(id = R.string.remember_email),
|
||||
isChecked = state.isRememberEmailEnabled,
|
||||
onCheckedChange = onRememberMeToggle,
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
|
@ -299,20 +294,20 @@ private fun LandingScreenContent(
|
|||
.wrapContentHeight(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.new_around_here),
|
||||
text = stringResource(id = R.string.new_to_bitwarden),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
)
|
||||
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.create_account),
|
||||
label = stringResource(id = R.string.create_an_account),
|
||||
onClick = onCreateAccountClick,
|
||||
modifier = Modifier
|
||||
.testTag("CreateAccountLabel"),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ class LandingViewModel @Inject constructor(
|
|||
?: LandingState(
|
||||
emailInput = authRepository.rememberedEmailAddress.orEmpty(),
|
||||
isContinueButtonEnabled = authRepository.rememberedEmailAddress != null,
|
||||
isRememberMeEnabled = authRepository.rememberedEmailAddress != null,
|
||||
isRememberEmailEnabled = authRepository.rememberedEmailAddress != null,
|
||||
selectedEnvironmentType = environmentRepository.environment.type,
|
||||
selectedEnvironmentLabel = environmentRepository.environment.label,
|
||||
dialog = null,
|
||||
|
@ -185,7 +185,7 @@ class LandingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
val email = state.emailInput
|
||||
val isRememberMeEnabled = state.isRememberMeEnabled
|
||||
val isRememberMeEnabled = state.isRememberEmailEnabled
|
||||
|
||||
// Update the remembered email address
|
||||
authRepository.rememberedEmailAddress = email.takeUnless { !isRememberMeEnabled }
|
||||
|
@ -210,7 +210,7 @@ class LandingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleRememberMeToggled(action: LandingAction.RememberMeToggle) {
|
||||
mutableStateFlow.update { it.copy(isRememberMeEnabled = action.isChecked) }
|
||||
mutableStateFlow.update { it.copy(isRememberEmailEnabled = action.isChecked) }
|
||||
}
|
||||
|
||||
private fun handleEnvironmentTypeSelect(action: LandingAction.EnvironmentTypeSelect) {
|
||||
|
@ -261,12 +261,18 @@ class LandingViewModel @Inject constructor(
|
|||
data class LandingState(
|
||||
val emailInput: String,
|
||||
val isContinueButtonEnabled: Boolean,
|
||||
val isRememberMeEnabled: Boolean,
|
||||
val isRememberEmailEnabled: Boolean,
|
||||
val selectedEnvironmentType: Environment.Type,
|
||||
val selectedEnvironmentLabel: String,
|
||||
val dialog: DialogState?,
|
||||
val accountSummaries: List<AccountSummary>,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Determines whether the app bar should be visible based on the presence of account summaries.
|
||||
*/
|
||||
val isAppBarVisible: Boolean
|
||||
get() = accountSummaries.isNotEmpty()
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on screen.
|
||||
*/
|
||||
|
|
|
@ -253,9 +253,9 @@ private fun LoginScreenContent(
|
|||
modifier = Modifier.testTag("GetMasterPasswordHintLabel"),
|
||||
)
|
||||
},
|
||||
passwordFieldTestTag = "MasterPasswordEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.testTag("MasterPasswordEntry")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
@ -232,7 +232,8 @@ private fun StartRegistrationContent(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
EnvironmentSelector(
|
||||
labelText = stringResource(id = R.string.creating_on),
|
||||
labelText = stringResource(id = R.string.create_account_on_with_colon),
|
||||
dialogTitle = stringResource(id = R.string.create_account_on),
|
||||
selectedOption = selectedEnvironmentType,
|
||||
onOptionSelected = handler.onEnvironmentTypeSelect,
|
||||
modifier = Modifier.testTag("RegionSelectorDropdown"),
|
||||
|
|
|
@ -269,8 +269,8 @@ private fun TwoFactorLoginScreenContent(
|
|||
}
|
||||
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.remember_me),
|
||||
isChecked = state.isRememberMeEnabled,
|
||||
label = stringResource(id = R.string.remember),
|
||||
isChecked = state.isRememberEnabled,
|
||||
onCheckedChange = onRememberMeToggle,
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
|
@ -317,7 +317,7 @@ private fun TwoFactorLoginScreenContentPreview() {
|
|||
dialogState = null,
|
||||
displayEmail = "email@dot.com",
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEnabled = true,
|
||||
captchaToken = null,
|
||||
email = "",
|
||||
password = "",
|
||||
|
|
|
@ -68,7 +68,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
.twoFactorResponse
|
||||
.preferredAuthMethod
|
||||
.isContinueButtonEnabled,
|
||||
isRememberMeEnabled = false,
|
||||
isRememberEnabled = false,
|
||||
captchaToken = null,
|
||||
email = args.emailAddress,
|
||||
password = args.password,
|
||||
|
@ -445,7 +445,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
private fun handleRememberMeToggle(action: TwoFactorLoginAction.RememberMeToggle) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isRememberMeEnabled = action.isChecked,
|
||||
isRememberEnabled = action.isChecked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -561,7 +561,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
|||
twoFactorData = TwoFactorDataModel(
|
||||
code = code,
|
||||
method = state.authMethod.value.toString(),
|
||||
remember = state.isRememberMeEnabled,
|
||||
remember = state.isRememberEnabled,
|
||||
),
|
||||
captchaToken = state.captchaToken,
|
||||
orgIdentifier = state.orgIdentifier,
|
||||
|
@ -586,7 +586,7 @@ data class TwoFactorLoginState(
|
|||
val dialogState: DialogState?,
|
||||
val displayEmail: String,
|
||||
val isContinueButtonEnabled: Boolean,
|
||||
val isRememberMeEnabled: Boolean,
|
||||
val isRememberEnabled: Boolean,
|
||||
// Internal
|
||||
val captchaToken: String?,
|
||||
val email: String,
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.semantics.testTagsAsResourceId
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled dialog for entering client certificate password and alias.
|
||||
*
|
||||
* @param onConfirmClick called when the confirm button is clicked and emits the input values.
|
||||
* @param onDismissRequest called when the user attempts to dismiss the dialog (for example by
|
||||
* tapping outside of it).
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun BitwardenClientCertificateDialog(
|
||||
onConfirmClick: (alias: String, password: String) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var alias by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.cancel),
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.testTag("DismissAlertButton"),
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.submit),
|
||||
isEnabled = password.isNotEmpty(),
|
||||
onClick = { onConfirmClick(alias, password) },
|
||||
modifier = Modifier.testTag("AcceptAlertButton"),
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.import_client_certificate),
|
||||
style = BitwardenTheme.typography.headlineSmall,
|
||||
modifier = Modifier.testTag("AlertTitleText"),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.enter_the_client_certificate_password_and_alias),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
modifier = Modifier.testTag("AlertContentText"),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BitwardenTextField(
|
||||
label = stringResource(R.string.alias),
|
||||
value = alias,
|
||||
onValueChange = { alias = it },
|
||||
autoFocus = true,
|
||||
cardStyle = CardStyle.Top(dividerPadding = 0.dp),
|
||||
textFieldTestTag = "AlertClientCertificateAliasInputField",
|
||||
modifier = Modifier.imePadding(),
|
||||
)
|
||||
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(R.string.password),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
cardStyle = CardStyle.Bottom,
|
||||
textFieldTestTag = "AlertClientCertificatePasswordInputField",
|
||||
modifier = Modifier.imePadding(),
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = BitwardenTheme.shapes.dialog,
|
||||
containerColor = BitwardenTheme.colorScheme.background.primary,
|
||||
iconContentColor = BitwardenTheme.colorScheme.icon.secondary,
|
||||
titleContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
textContentColor = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = modifier.semantics {
|
||||
testTagsAsResourceId = true
|
||||
testTag = "AlertPopup"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun BitwardenClientCertificateDialogPreview() {
|
||||
BitwardenTheme {
|
||||
BitwardenClientCertificateDialog(
|
||||
onConfirmClick = { alias, password -> },
|
||||
onDismissRequest = {},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.util.displayLabel
|
|||
* and displays the currently selected region on the UI.
|
||||
*
|
||||
* @param labelText The text displayed near the selector button.
|
||||
* @param dialogTitle The title displayed in the selection dialog.
|
||||
* @param selectedOption The currently selected environment option.
|
||||
* @param onOptionSelected A callback that gets invoked when an environment option is selected
|
||||
* and passes the selected option as an argument.
|
||||
|
@ -43,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.util.displayLabel
|
|||
@Composable
|
||||
fun EnvironmentSelector(
|
||||
labelText: String,
|
||||
dialogTitle: String,
|
||||
selectedOption: Environment.Type,
|
||||
onOptionSelected: (Environment.Type) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -64,12 +66,12 @@ fun EnvironmentSelector(
|
|||
Text(
|
||||
text = labelText,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
Text(
|
||||
text = selectedOption.displayLabel(),
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
color = BitwardenTheme.colorScheme.text.interaction,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
|
@ -82,7 +84,7 @@ fun EnvironmentSelector(
|
|||
|
||||
if (shouldShowDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(id = R.string.logging_in_on),
|
||||
title = dialogTitle,
|
||||
onDismissRequest = { shouldShowDialog = false },
|
||||
) {
|
||||
options.forEach {
|
||||
|
|
44
app/src/main/res/drawable/bitwarden_logo.xml
Normal file
44
app/src/main/res/drawable/bitwarden_logo.xml
Normal file
|
@ -0,0 +1,44 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="218dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="218"
|
||||
android:viewportHeight="34">
|
||||
<path
|
||||
android:pathData="M58.34,11C56.91,9.04 54.97,8.09 52.47,8.09C49.85,8.09 47.83,9.16 46.52,11.2H46.28C46.44,9.31 46.52,7.94 46.52,7.07V1.02C46.52,0.63 46.2,0.31 45.8,0.31H41.52C41.12,0.31 40.8,0.63 40.8,1.02V28.64C40.8,29.04 41.12,29.35 41.52,29.35H44.69C44.97,29.35 45.25,29.19 45.37,28.92L46.12,27.07H46.52C47.95,28.88 49.85,29.74 52.35,29.74C54.85,29.74 56.83,28.84 58.3,26.92C59.77,25.03 60.48,22.36 60.48,18.9C60.48,15.56 59.77,12.93 58.34,11ZM47.51,13.99C48.18,13.12 49.26,12.69 50.64,12.69C51.83,12.69 52.83,13.2 53.54,14.22C54.22,15.25 54.57,16.78 54.57,18.82C54.57,20.82 54.22,22.44 53.54,23.5C52.87,24.6 51.87,25.15 50.72,25.15C49.26,25.15 48.18,24.64 47.51,23.69C46.84,22.75 46.52,21.14 46.52,18.9V18.27C46.52,16.31 46.88,14.85 47.51,13.99Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M65.64,29.39H69.93C70.32,29.39 70.68,29 70.64,28.64V9.23C70.64,8.84 70.32,8.53 69.93,8.53H65.64C65.25,8.53 64.93,8.84 64.93,9.23V28.68C64.93,29.08 65.25,29.39 65.64,29.39Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M88.61,24.71C87.42,25.03 86.43,25.19 85.6,25.19C84.89,25.19 84.25,24.99 83.85,24.56C83.42,24.2 83.18,23.58 83.18,22.79V12.81H88.57C88.85,12.81 89.05,12.61 89.05,12.34V8.8C89.05,8.68 88.93,8.57 88.81,8.57H83.14V4.6C83.14,4.32 82.94,4.13 82.66,4.13H79.81C79.61,4.13 79.45,4.24 79.37,4.44L77.82,8.53L74.69,10.37C74.69,10.39 74.68,10.41 74.67,10.43C74.66,10.45 74.65,10.47 74.65,10.49V12.34C74.65,12.61 74.85,12.81 75.12,12.81H77.39V22.83C77.39,25.11 77.94,26.84 78.97,27.98C80,29.15 81.67,29.7 83.97,29.7C85.88,29.7 87.58,29.43 88.93,28.88C89.09,28.8 89.21,28.64 89.21,28.45V25.19C89.21,24.87 88.93,24.64 88.61,24.71Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M111.07,28.53C111.31,29.04 111.75,29.39 112.3,29.39H112.46C113.06,29.39 113.53,29.04 113.69,28.49L118.69,10.22C118.85,9.63 118.41,9.08 117.82,9.08C117.42,9.08 117.06,9.35 116.94,9.74L113.97,20.71C113.06,24.24 112.58,26.44 112.46,27.23H112.34C112.14,26.25 111.59,24.36 110.68,21.41L106.94,9.86C106.79,9.39 106.35,9.08 105.83,9.08C105.28,9.08 104.84,9.39 104.68,9.86L100.68,21.49C100.32,22.48 99.76,24.4 99.05,27.31H98.93C98.69,25.89 98.18,23.77 97.42,20.86L94.33,9.74C94.21,9.35 93.85,9.08 93.46,9.08H93.34C92.7,9.08 92.26,9.67 92.42,10.22L97.7,28.49C97.86,29.04 98.34,29.39 98.89,29.39C99.45,29.39 99.92,29.08 100.08,28.57L104.41,15.87L105.24,13.2L105.64,11.79H105.75C105.86,12.19 105.97,12.58 106.07,12.94C106.39,14.16 106.65,15.14 106.87,15.83L111.07,28.53Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M135.15,28.76C135.2,29.12 135.51,29.39 135.87,29.39C136.27,29.39 136.62,29 136.66,28.76V15.99C136.66,13.6 136.11,11.75 135,10.61C133.85,9.43 132.18,8.84 129.92,8.84C127.93,8.84 125.99,9.27 124.01,10.06C123.57,10.22 123.37,10.73 123.57,11.16C123.77,11.59 124.28,11.79 124.72,11.59C126.47,10.77 128.13,10.37 129.8,10.37C131.54,10.37 132.77,10.84 133.61,11.83C134.44,12.81 134.84,14.22 134.84,16.19V17.52L130.95,17.64C127.74,17.64 125.36,18.31 123.65,19.41C121.98,20.55 121.11,22.51 121.27,24.56C121.35,26.09 121.9,27.31 122.89,28.21C124.01,29.23 125.55,29.74 127.58,29.74C129.04,29.74 130.35,29.51 131.43,28.96C132.54,28.41 133.57,27.47 134.6,26.17H134.8L135.15,28.76ZM132.81,26.21C131.58,27.43 129.88,28.05 127.66,28.05C126.23,28.05 125.12,27.7 124.24,27.11C123.49,26.4 123.09,25.38 123.09,24.13C123.09,22.55 123.73,21.34 124.96,20.59C126.23,19.84 128.25,19.37 131.15,19.25L134.72,19.1V21.06C134.72,23.22 134.08,24.99 132.81,26.21Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M153.01,8.8C152.34,8.72 151.74,8.68 151.15,8.68C149.8,8.68 148.65,9 147.73,9.63C146.78,10.22 145.87,11.28 145.04,12.77H144.92L144.76,9.86C144.76,9.43 144.4,9.12 143.96,9.12C143.53,9.12 143.17,9.47 143.17,9.9V28.37C143.17,28.88 143.57,29.27 144.08,29.27C144.6,29.27 144.99,28.88 144.99,28.37V18.07C144.99,15.87 145.55,13.99 146.66,12.53C147.77,11.08 149.2,10.33 150.95,10.33C151.54,10.33 152.14,10.41 152.73,10.49C153.21,10.57 153.68,10.29 153.76,9.82C153.84,9.35 153.53,8.88 153.01,8.8Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M168.96,9.47C167.89,8.96 166.58,8.68 165.11,8.68C162.25,8.68 160.11,9.63 158.6,11.32C157.1,13.2 156.38,15.87 156.38,19.37C156.38,22.71 157.18,25.23 158.64,26.99C160.19,28.76 162.29,29.63 165.15,29.63C168.05,29.63 170.31,28.49 171.86,26.17H172.01L172.41,28.72C172.45,29.08 172.73,29.31 173.05,29.31C173.4,29.31 173.68,29.04 173.68,28.68V1.22C173.68,0.71 173.29,0.31 172.77,0.31C172.25,0.31 171.86,0.71 171.86,1.22V7.62C171.86,9.12 171.9,10.61 171.98,12.22H171.86C171.02,10.92 170.07,9.98 168.96,9.47ZM160.03,12.73C161.18,11.16 162.85,10.37 165.11,10.37C167.49,10.37 169.2,11.04 170.23,12.46C171.34,13.79 171.86,16.03 171.86,19.17V19.49C171.86,22.55 171.34,24.75 170.23,26.09C169.16,27.43 167.49,28.09 165.15,28.09C160.63,28.09 158.37,25.23 158.37,19.49C158.37,16.5 158.92,14.26 160.03,12.73Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M181.89,26.99C183.6,28.84 185.9,29.74 188.88,29.74C190.07,29.74 191.14,29.67 192.13,29.39C192.96,29.23 193.88,29 194.83,28.6C195.15,28.49 195.35,28.17 195.35,27.86C195.35,27.31 194.79,26.92 194.27,27.11C193.36,27.43 192.61,27.62 191.97,27.74C191.1,27.9 190.03,27.98 188.88,27.98C186.46,27.98 184.63,27.31 183.32,25.82C182.01,24.32 181.38,22.2 181.34,19.41H196.1V17.92C196.1,15.05 195.42,12.77 194,11.12C192.65,9.47 190.7,8.64 188.32,8.64C185.58,8.64 183.44,9.63 181.82,11.59C180.19,13.52 179.4,16.11 179.4,19.37C179.4,22.63 180.23,25.19 181.89,26.99ZM183.56,12.26C184.75,10.96 186.34,10.33 188.32,10.33C190.15,10.33 191.57,11 192.61,12.3C193.64,13.6 194.15,15.48 194.15,17.8H181.46C181.7,15.4 182.37,13.52 183.56,12.26Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M216.13,28.49C216.13,29 216.53,29.39 217.05,29.39C217.52,29.39 217.96,28.96 218,28.41V16.07C218,11.12 215.66,8.64 211.02,8.64C207.6,8.64 205.26,9.71 203.91,11.83H203.8L203.56,9.78C203.48,9.39 203.16,9.08 202.73,9.08C202.25,9.08 201.89,9.43 201.89,9.9V28.41C201.89,28.92 202.29,29.31 202.8,29.31C203.32,29.31 203.72,28.92 203.72,28.41V18.11C203.72,15.4 204.35,13.4 205.46,12.18C206.57,10.92 208.36,10.33 210.82,10.33C212.64,10.33 213.95,10.84 214.87,11.75C215.7,12.65 216.13,14.15 216.13,16.19V28.49Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M64.45,3.18C64.45,1.41 65.96,0 67.82,0C69.65,0 71.2,1.41 71.2,3.22V3.5C71.2,5.23 69.65,6.68 67.82,6.68C66,6.68 64.45,5.23 64.45,3.5V3.18Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M28.34,18.55V1.69C28.34,1.31 28.2,0.98 27.92,0.7C27.64,0.42 27.3,0.28 26.92,0.28H1.42C1.03,0.28 0.7,0.42 0.42,0.7C0.14,0.98 0,1.31 0,1.69V18.55C0,19.8 0.25,21.05 0.74,22.29C1.24,23.52 1.85,24.62 2.58,25.58C3.31,26.54 4.18,27.47 5.19,28.38C6.2,29.29 7.14,30.04 7.99,30.64C8.85,31.24 9.74,31.81 10.67,32.34C11.6,32.88 12.26,33.24 12.65,33.43C13.04,33.62 13.36,33.77 13.59,33.87C13.77,33.96 13.96,34 14.17,34C14.37,34 14.57,33.96 14.74,33.87C14.98,33.77 15.29,33.62 15.68,33.43C16.08,33.24 16.74,32.88 17.67,32.34C18.6,31.81 19.49,31.24 20.34,30.64C21.2,30.04 22.13,29.29 23.15,28.38C24.16,27.47 25.03,26.54 25.76,25.58C26.49,24.62 27.1,23.52 27.59,22.29C28.09,21.05 28.34,19.8 28.34,18.55ZM24.09,4.5V18.55C24.09,21.12 22.35,23.76 18.88,26.45C17.5,27.53 15.92,28.53 14.17,29.46V4.5H24.09Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -1,44 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="220dp"
|
||||
android:height="74dp"
|
||||
android:viewportWidth="220"
|
||||
android:viewportHeight="74">
|
||||
<path
|
||||
android:pathData="M69.38,31.56C68.23,29.97 66.68,29.21 64.68,29.21C62.58,29.21 60.96,30.07 59.91,31.72H59.72C59.85,30.2 59.91,29.09 59.91,28.39V23.5C59.91,23.19 59.66,22.93 59.34,22.93H55.91C55.59,22.93 55.34,23.19 55.34,23.5V45.8C55.34,46.12 55.59,46.37 55.91,46.37H58.45C58.67,46.37 58.9,46.24 58.99,46.02L59.6,44.53H59.91C61.06,45.99 62.58,46.69 64.58,46.69C66.58,46.69 68.17,45.96 69.35,44.41C70.52,42.88 71.09,40.73 71.09,37.94C71.09,35.24 70.52,33.11 69.38,31.56ZM60.71,33.97C61.25,33.27 62.1,32.92 63.22,32.92C64.17,32.92 64.96,33.34 65.53,34.16C66.07,34.99 66.36,36.22 66.36,37.87C66.36,39.49 66.07,40.79 65.53,41.65C64.99,42.53 64.2,42.98 63.28,42.98C62.1,42.98 61.25,42.57 60.71,41.8C60.17,41.04 59.91,39.74 59.91,37.94V37.43C59.91,35.84 60.2,34.67 60.71,33.97Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M75.22,46.4H78.65C78.97,46.4 79.25,46.09 79.22,45.8V30.13C79.22,29.82 78.97,29.56 78.65,29.56H75.22C74.9,29.56 74.65,29.82 74.65,30.13V45.83C74.65,46.15 74.9,46.4 75.22,46.4Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M93.61,42.63C92.65,42.88 91.86,43.01 91.19,43.01C90.62,43.01 90.11,42.85 89.8,42.5C89.45,42.22 89.26,41.71 89.26,41.08V33.02H93.58C93.8,33.02 93.96,32.86 93.96,32.64V29.78C93.96,29.69 93.86,29.59 93.77,29.59H89.22V26.39C89.22,26.17 89.07,26.01 88.84,26.01H86.56C86.4,26.01 86.27,26.11 86.21,26.26L84.97,29.56L82.46,31.05C82.46,31.07 82.45,31.08 82.44,31.1C82.44,31.12 82.43,31.13 82.43,31.15V32.64C82.43,32.86 82.59,33.02 82.81,33.02H84.62V41.11C84.62,42.95 85.06,44.34 85.89,45.26C86.72,46.21 88.05,46.66 89.89,46.66C91.42,46.66 92.78,46.44 93.86,45.99C93.99,45.93 94.08,45.8 94.08,45.64V43.01C94.08,42.76 93.86,42.57 93.61,42.63Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M111.58,45.71C111.77,46.12 112.12,46.4 112.57,46.4H112.69C113.17,46.4 113.55,46.12 113.68,45.67L117.68,30.93C117.81,30.45 117.46,30.01 116.98,30.01C116.66,30.01 116.38,30.23 116.28,30.55L113.9,39.39C113.17,42.25 112.79,44.02 112.69,44.66H112.6C112.44,43.87 112,42.34 111.26,39.97L108.28,30.64C108.15,30.26 107.8,30.01 107.39,30.01C106.94,30.01 106.6,30.26 106.47,30.64L103.26,40.03C102.98,40.82 102.53,42.38 101.96,44.72H101.86C101.67,43.58 101.26,41.87 100.66,39.52L98.18,30.55C98.09,30.23 97.8,30.01 97.48,30.01H97.39C96.88,30.01 96.53,30.48 96.66,30.93L100.88,45.67C101.01,46.12 101.39,46.4 101.83,46.4C102.28,46.4 102.66,46.15 102.79,45.74L106.25,35.49L106.91,33.34L107.23,32.19H107.33C107.41,32.52 107.5,32.83 107.57,33.13C107.83,34.11 108.04,34.9 108.22,35.46L111.58,45.71Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M130.86,45.9C130.89,46.18 131.14,46.4 131.43,46.4C131.75,46.4 132.03,46.09 132.07,45.9V35.59C132.07,33.65 131.62,32.16 130.73,31.24C129.81,30.29 128.48,29.82 126.67,29.82C125.08,29.82 123.52,30.17 121.93,30.8C121.58,30.93 121.43,31.34 121.58,31.69C121.74,32.04 122.16,32.19 122.51,32.04C123.9,31.37 125.24,31.05 126.57,31.05C127.97,31.05 128.95,31.43 129.62,32.23C130.29,33.02 130.6,34.16 130.6,35.75V36.83L127.49,36.92C124.92,36.92 123.01,37.46 121.65,38.35C120.32,39.27 119.62,40.85 119.74,42.5C119.81,43.74 120.25,44.72 121.04,45.45C121.93,46.28 123.17,46.69 124.79,46.69C125.97,46.69 127.02,46.5 127.87,46.05C128.76,45.61 129.59,44.85 130.41,43.8H130.57L130.86,45.9ZM128.99,43.83C128,44.82 126.64,45.32 124.86,45.32C123.71,45.32 122.82,45.04 122.13,44.56C121.52,43.99 121.2,43.17 121.2,42.15C121.2,40.88 121.71,39.9 122.7,39.3C123.71,38.7 125.33,38.32 127.65,38.22L130.51,38.09V39.68C130.51,41.42 130,42.85 128.99,43.83Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M145.15,29.78C144.61,29.72 144.13,29.69 143.66,29.69C142.58,29.69 141.66,29.94 140.93,30.45C140.16,30.93 139.43,31.78 138.77,32.99H138.67L138.54,30.64C138.54,30.29 138.26,30.04 137.91,30.04C137.56,30.04 137.27,30.32 137.27,30.67V45.58C137.27,45.99 137.59,46.31 138,46.31C138.42,46.31 138.73,45.99 138.73,45.58V37.27C138.73,35.49 139.18,33.97 140.07,32.8C140.96,31.62 142.1,31.02 143.5,31.02C143.97,31.02 144.45,31.08 144.93,31.15C145.31,31.21 145.69,30.99 145.75,30.61C145.82,30.23 145.56,29.85 145.15,29.78Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M157.91,30.32C157.06,29.91 156.01,29.69 154.84,29.69C152.55,29.69 150.83,30.45 149.63,31.81C148.42,33.34 147.85,35.49 147.85,38.32C147.85,41.01 148.48,43.04 149.66,44.47C150.9,45.9 152.58,46.59 154.87,46.59C157.18,46.59 158.99,45.67 160.23,43.8H160.36L160.68,45.86C160.71,46.15 160.93,46.34 161.19,46.34C161.47,46.34 161.69,46.12 161.69,45.83V23.66C161.69,23.25 161.38,22.93 160.96,22.93C160.55,22.93 160.23,23.25 160.23,23.66V28.83C160.23,30.04 160.26,31.24 160.33,32.54H160.23C159.57,31.5 158.8,30.74 157.91,30.32ZM150.77,32.96C151.69,31.69 153.02,31.05 154.84,31.05C156.74,31.05 158.11,31.59 158.93,32.73C159.82,33.81 160.23,35.62 160.23,38.16V38.41C160.23,40.88 159.82,42.66 158.93,43.74C158.07,44.82 156.74,45.36 154.87,45.36C151.25,45.36 149.44,43.04 149.44,38.41C149.44,36 149.88,34.19 150.77,32.96Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M168.27,44.47C169.63,45.96 171.48,46.69 173.86,46.69C174.81,46.69 175.67,46.63 176.46,46.4C177.13,46.28 177.86,46.09 178.62,45.77C178.88,45.67 179.03,45.42 179.03,45.17C179.03,44.72 178.59,44.41 178.18,44.56C177.45,44.82 176.84,44.98 176.33,45.07C175.64,45.2 174.78,45.26 173.86,45.26C171.92,45.26 170.46,44.72 169.41,43.52C168.36,42.31 167.85,40.6 167.82,38.35H179.64V37.14C179.64,34.83 179.1,32.99 177.95,31.66C176.87,30.32 175.32,29.66 173.41,29.66C171.22,29.66 169.51,30.45 168.21,32.04C166.9,33.59 166.27,35.68 166.27,38.32C166.27,40.95 166.93,43.01 168.27,44.47ZM169.6,32.58C170.55,31.53 171.82,31.02 173.41,31.02C174.87,31.02 176.02,31.56 176.84,32.61C177.67,33.65 178.08,35.18 178.08,37.05H167.92C168.11,35.11 168.65,33.59 169.6,32.58Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M195.67,45.67C195.67,46.09 195.99,46.4 196.4,46.4C196.79,46.4 197.13,46.05 197.17,45.61V35.65C197.17,31.66 195.29,29.66 191.58,29.66C188.85,29.66 186.97,30.51 185.89,32.23H185.8L185.61,30.58C185.54,30.26 185.29,30.01 184.94,30.01C184.56,30.01 184.27,30.29 184.27,30.67V45.61C184.27,46.02 184.59,46.34 185,46.34C185.42,46.34 185.73,46.02 185.73,45.61V37.3C185.73,35.11 186.24,33.5 187.13,32.51C188.02,31.5 189.45,31.02 191.42,31.02C192.88,31.02 193.93,31.43 194.66,32.16C195.32,32.89 195.67,34.1 195.67,35.75V45.67Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M74.27,25.25C74.27,23.82 75.47,22.68 76.97,22.68C78.43,22.68 79.67,23.82 79.67,25.28V25.5C79.67,26.9 78.43,28.07 76.97,28.07C75.51,28.07 74.27,26.9 74.27,25.5V25.25Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M45.36,37.65V24.04C45.36,23.73 45.25,23.47 45.02,23.24C44.8,23.02 44.53,22.91 44.23,22.91H23.81C23.51,22.91 23.24,23.02 23.02,23.24C22.79,23.47 22.68,23.73 22.68,24.04V37.65C22.68,38.67 22.88,39.67 23.27,40.67C23.67,41.67 24.16,42.55 24.74,43.33C25.33,44.1 26.03,44.86 26.84,45.59C27.64,46.32 28.39,46.93 29.08,47.41C29.76,47.9 30.48,48.35 31.22,48.79C31.97,49.22 32.49,49.51 32.81,49.66C33.12,49.82 33.37,49.93 33.56,50.02C33.7,50.09 33.86,50.12 34.02,50.12C34.19,50.12 34.34,50.09 34.48,50.02C34.67,49.93 34.92,49.82 35.23,49.66C35.55,49.51 36.08,49.22 36.82,48.79C37.56,48.35 38.28,47.9 38.96,47.41C39.65,46.93 40.4,46.32 41.21,45.59C42.01,44.86 42.71,44.1 43.3,43.33C43.88,42.55 44.37,41.67 44.77,40.67C45.16,39.67 45.36,38.67 45.36,37.65ZM41.96,26.31V37.65C41.96,39.73 40.57,41.85 37.79,44.03C36.68,44.9 35.43,45.71 34.02,46.46V26.31H41.96Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -103,6 +103,7 @@
|
|||
<string name="close">Close</string>
|
||||
<string name="continue_text">Continue</string>
|
||||
<string name="create_account">Create account</string>
|
||||
<string name="create_an_account">Create an account</string>
|
||||
<string name="creating_account">Creating account...</string>
|
||||
<string name="edit_item">Edit item</string>
|
||||
<string name="enable_automatic_syncing">Allow automatic syncing</string>
|
||||
|
@ -136,7 +137,7 @@
|
|||
<string name="vault_timeout_action">Vault timeout action</string>
|
||||
<string name="vault_timeout_log_out_confirmation">Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?</string>
|
||||
<string name="logging_in">Logging in...</string>
|
||||
<string name="login_or_create_new_account">Log in or create a new account to access your secure vault.</string>
|
||||
<string name="login_to_bitwarden">Log in to Bitwarden</string>
|
||||
<string name="manage">Manage</string>
|
||||
<string name="master_password_confirmation_val_message">Password confirmation is not correct.</string>
|
||||
<string name="master_password_description">The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it.</string>
|
||||
|
@ -224,7 +225,8 @@
|
|||
<string name="login_unavailable">Login unavailable</string>
|
||||
<string name="no_two_step_available">This account has two-step login set up, however, none of the configured two-step providers are supported on this device. Please use a supported device and/or add additional providers that are better supported across devices (such as an authenticator app).</string>
|
||||
<string name="recovery_code_title">Recovery code</string>
|
||||
<string name="remember_me">Remember me</string>
|
||||
<string name="remember">Remember</string>
|
||||
<string name="remember_email">Remember email</string>
|
||||
<string name="send_verification_code_again">Send verification code email again</string>
|
||||
<string name="two_step_login_options">Two-step login options</string>
|
||||
<string name="use_another_two_step_method">Use another two-step login method</string>
|
||||
|
@ -752,7 +754,7 @@ select Add TOTP to store the key safely</string>
|
|||
<string name="login_attempt_from_x_do_you_want_to_switch_to_this_account">Login attempt from:
|
||||
%1$s
|
||||
Do you want to switch to this account?</string>
|
||||
<string name="new_around_here">New around here?</string>
|
||||
<string name="new_to_bitwarden">New to Bitwarden?</string>
|
||||
<string name="get_master_passwordword_hint">Get master password hint</string>
|
||||
<string name="logging_in_as_x_on_y">Logging in as %1$s on %2$s</string>
|
||||
<string name="not_you">Not you?</string>
|
||||
|
@ -841,6 +843,7 @@ Do you want to switch to this account?</string>
|
|||
<string name="log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option">Log in with device must be set up in the settings of the Bitwarden app. Need another option?</string>
|
||||
<string name="log_in_with_device">Log in with device</string>
|
||||
<string name="logging_in_on">Logging in on</string>
|
||||
<string name="logging_in_on_with_colon">Logging in on:</string>
|
||||
<string name="vault">Vault</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="account_security">Account security</string>
|
||||
|
@ -935,7 +938,8 @@ Do you want to switch to this account?</string>
|
|||
<string name="self_hosted_server_url">Self-hosted server URL</string>
|
||||
<string name="passkey_operation_failed_because_user_could_not_be_verified">Passkey operation failed because user could not be verified.</string>
|
||||
<string name="user_verification_direction">User verification</string>
|
||||
<string name="creating_on">Creating on:</string>
|
||||
<string name="create_account_on">Create account on</string>
|
||||
<string name="create_account_on_with_colon">Create account on:</string>
|
||||
<string name="follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account">Follow the instructions in the email sent to %1$s to continue creating your account.</string>
|
||||
<string name="we_sent_an_email_to">We sent an email to <annotation emphasis="bold"><annotation arg="0">%1$s</annotation></annotation>.</string>
|
||||
<string name="by_continuing_you_agree_to_the_terms_of_service_and_privacy_policy">By continuing, you agree to the <annotation link="termsOfService">Terms of Service</annotation> and <annotation link="privacyPolicy">Privacy Policy</annotation></string>
|
||||
|
@ -1125,4 +1129,7 @@ Do you want to switch to this account?</string>
|
|||
<string name="we_ll_walk_you_through_the_key_features_to_add_a_new_login">We\'ll walk you through the key features to add a new login.</string>
|
||||
<string name="explore_the_generator">Explore the generator</string>
|
||||
<string name="learn_more_about_generating_secure_login_credentials_with_guided_tour">Learn more about generating secure login credentials with a guided tour.</string>
|
||||
<string name="import_client_certificate">Import client certificate</string>
|
||||
<string name="enter_the_client_certificate_password_and_alias">Enter the client certificate password and the desired alias for this certificate.</string>
|
||||
<string name="alias">Alias</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,649 @@
|
|||
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.datasource.disk.model.ImportPrivateKeyResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsCertificate
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.model.MutualTlsKeyHost
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.PrivateKey
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
class KeyManagerTest {
|
||||
private val mockContext = mockk<Context>()
|
||||
private val mockAndroidKeyStore = mockk<KeyStore>(name = "MockAndroidKeyStore")
|
||||
private val mockPkcs12KeyStore = mockk<KeyStore>(name = "MockPKCS12KeyStore")
|
||||
private val keyDiskSource = KeyManagerImpl(
|
||||
context = mockContext,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(KeyStore::class, KeyChain::class)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(KeyStore::class, KeyChain::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when MutualTlsKeyAlias is not found`() {
|
||||
// Verify null is returned when alias is not found in KeyChain
|
||||
setupMockAndroidKeyStore()
|
||||
every { KeyChain.getPrivateKey(mockContext, "mockAlias") } throws KeyChainException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = "mockAlias",
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify null is returned when alias is not found in AndroidKeyStore
|
||||
every { mockAndroidKeyStore.getKey("mockAlias", null) } throws UnrecoverableKeyException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = "mockAlias",
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return MutualTlsCertificateChain when using ANDROID KEY STORE and key is found`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val mockAlias = "mockAlias"
|
||||
val mockPrivateKey = mockk<PrivateKey>()
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
every {
|
||||
mockAndroidKeyStore.getCertificateChain(mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} returns mockPrivateKey
|
||||
|
||||
val result = keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
MutualTlsCertificate(
|
||||
alias = mockAlias,
|
||||
certificateChain = listOf(mockCertificate1, mockCertificate2),
|
||||
privateKey = mockPrivateKey,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when using ANDROID KEY STORE and key is not found`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val mockAlias = "mockAlias"
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
every {
|
||||
mockAndroidKeyStore.getCertificateChain(mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} returns null
|
||||
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when using ANDROID KEY STORE and certificate chain is invalid`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val mockAlias = "mockAlias"
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} returns mockk<PrivateKey>()
|
||||
|
||||
// Verify null is returned when certificate chain is empty
|
||||
every {
|
||||
mockAndroidKeyStore.getCertificateChain(mockAlias)
|
||||
} returns emptyArray()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify null is returned when certificate chain contains non-X509Certificate objects
|
||||
every {
|
||||
mockAndroidKeyStore.getCertificateChain(mockAlias)
|
||||
} returns arrayOf(mockk<Certificate>())
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when using ANDROID KEY STORE and an exception occurs`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val mockAlias = "mockAlias"
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
every {
|
||||
mockAndroidKeyStore.getCertificateChain(mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
|
||||
// Verify KeyStoreException is handled
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} throws KeyStoreException()
|
||||
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify UnrecoverableKeyException is handled
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} throws UnrecoverableKeyException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify NoSuchAlgorithmException is handled
|
||||
every {
|
||||
mockAndroidKeyStore.getKey(mockAlias, null)
|
||||
} throws NoSuchAlgorithmException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return MutualTlsCertificateChain when using KEY CHAIN and key is found`() {
|
||||
val mockAlias = "mockAlias"
|
||||
val mockPrivateKey = mockk<PrivateKey>()
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
every {
|
||||
KeyChain.getCertificateChain(mockContext, mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
every {
|
||||
KeyChain.getPrivateKey(mockContext, mockAlias)
|
||||
} returns mockPrivateKey
|
||||
|
||||
val result = keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
MutualTlsCertificate(
|
||||
alias = mockAlias,
|
||||
certificateChain = listOf(mockCertificate1, mockCertificate2),
|
||||
privateKey = mockPrivateKey,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when using KEY CHAIN and key is not found`() {
|
||||
val mockAlias = "mockAlias"
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
every {
|
||||
KeyChain.getCertificateChain(mockContext, mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
every {
|
||||
KeyChain.getPrivateKey(mockContext, mockAlias)
|
||||
} returns null
|
||||
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `getMutualTlsCertificateChain should return null when using KEY CHAIN and an exception occurs`() {
|
||||
val mockAlias = "mockAlias"
|
||||
val mockCertificate1 = mockk<X509Certificate>(name = "mockCertificate1")
|
||||
val mockCertificate2 = mockk<X509Certificate>(name = "mockCertificate2")
|
||||
|
||||
every {
|
||||
KeyChain.getCertificateChain(mockContext, mockAlias)
|
||||
} returns arrayOf(mockCertificate1, mockCertificate2)
|
||||
|
||||
// Verify KeyChainException from getPrivateKey is handled
|
||||
every {
|
||||
KeyChain.getPrivateKey(mockContext, mockAlias)
|
||||
} throws KeyChainException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify KeyChainException from getCertificateChain is handled
|
||||
every { KeyChain.getPrivateKey(mockContext, mockAlias) } returns mockk()
|
||||
every { KeyChain.getCertificateChain(mockContext, mockAlias) } throws KeyChainException()
|
||||
assertNull(
|
||||
keyDiskSource.getMutualTlsCertificateChain(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `removeMutualTlsKey should remove key from AndroidKeyStore when host is ANDROID_KEY_STORE`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val mockAlias = "mockAlias"
|
||||
|
||||
every { mockAndroidKeyStore.deleteEntry(mockAlias) } just runs
|
||||
|
||||
keyDiskSource.removeMutualTlsKey(
|
||||
alias = mockAlias,
|
||||
host = MutualTlsKeyHost.ANDROID_KEY_STORE,
|
||||
)
|
||||
|
||||
verify {
|
||||
mockAndroidKeyStore.deleteEntry(mockAlias)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeMutualTlsKey should do nothing when host is KEY_CHAIN`() {
|
||||
keyDiskSource.removeMutualTlsKey(
|
||||
alias = "mockAlias",
|
||||
host = MutualTlsKeyHost.KEY_CHAIN,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
mockAndroidKeyStore.deleteEntry(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return Success when key is imported successfully`() {
|
||||
setupMockAndroidKeyStore()
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val internalAlias = "mockInternalAlias"
|
||||
val privateKey = mockk<PrivateKey>()
|
||||
val certChain = arrayOf(mockk<X509Certificate>())
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
every { mockPkcs12KeyStore.aliases() } returns mockk {
|
||||
every { hasMoreElements() } returns true
|
||||
every { nextElement() } returns internalAlias
|
||||
}
|
||||
every {
|
||||
mockPkcs12KeyStore.setKeyEntry(
|
||||
internalAlias,
|
||||
privateKey,
|
||||
null,
|
||||
certChain,
|
||||
)
|
||||
} just runs
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
internalAlias,
|
||||
password.toCharArray(),
|
||||
)
|
||||
} returns privateKey
|
||||
every {
|
||||
mockPkcs12KeyStore.getCertificateChain(internalAlias)
|
||||
} returns certChain
|
||||
every {
|
||||
mockAndroidKeyStore.containsAlias(expectedAlias)
|
||||
} returns false
|
||||
every {
|
||||
mockAndroidKeyStore.setKeyEntry(expectedAlias, privateKey, null, certChain)
|
||||
} just runs
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Success(alias = expectedAlias),
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return Error when loading PKCS12 throws an exception`() {
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
// Verify KeyStoreException is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws KeyStoreException()
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnsupportedKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
) { "KeyStoreException was not handled correctly" }
|
||||
|
||||
// Verify IOException is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws IOException()
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
) { "IOException was not handled correctly" }
|
||||
|
||||
// Verify IOException with UnrecoverableKeyException cause is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws IOException(UnrecoverableKeyException())
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify IOException with unexpected cause is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws IOException(Exception())
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
) { "IOException with Unexpected exception cause was not handled correctly" }
|
||||
|
||||
// Verify CertificateException is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws CertificateException()
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.InvalidCertificateChain,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
) { "CertificateException was not handled correctly" }
|
||||
|
||||
// Verify NoSuchAlgorithmException is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.load(any(), any())
|
||||
} throws NoSuchAlgorithmException()
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnsupportedKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
) { "NoSuchAlgorithmException was not handled correctly" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return UnsupportedKey when key store is empty`() {
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
every { mockPkcs12KeyStore.aliases() } returns mockk {
|
||||
every { hasMoreElements() } returns false
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnsupportedKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return UnrecoverableKey when unable to retrieve private key`() {
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
every {
|
||||
mockPkcs12KeyStore.aliases()
|
||||
} returns mockk {
|
||||
every { hasMoreElements() } returns true
|
||||
every { nextElement() } returns "mockInternalAlias"
|
||||
}
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
"mockInternalAlias",
|
||||
password.toCharArray(),
|
||||
)
|
||||
} throws UnrecoverableKeyException()
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
"mockInternalAlias",
|
||||
password.toCharArray(),
|
||||
)
|
||||
} returns null
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.UnrecoverableKey,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return InvalidCertificateChain when certificate chain is empty`() {
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
every { mockPkcs12KeyStore.aliases() } returns mockk {
|
||||
every { hasMoreElements() } returns true
|
||||
every { nextElement() } returns "mockInternalAlias"
|
||||
}
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
"mockInternalAlias",
|
||||
password.toCharArray(),
|
||||
)
|
||||
} returns mockk()
|
||||
|
||||
// Verify empty certificate chain is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
|
||||
} returns emptyArray()
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.InvalidCertificateChain,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
|
||||
// Verify null certificate chain is handled
|
||||
every {
|
||||
mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
|
||||
} returns null
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.InvalidCertificateChain,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return KeyStoreOperationFailed when saving to Android KeyStore throws KeyStoreException`() {
|
||||
setupMockAndroidKeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
every { mockPkcs12KeyStore.aliases() } returns mockk {
|
||||
every { hasMoreElements() } returns true
|
||||
every { nextElement() } returns "mockInternalAlias"
|
||||
}
|
||||
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
"mockInternalAlias",
|
||||
password.toCharArray(),
|
||||
)
|
||||
} returns mockk()
|
||||
every {
|
||||
mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
|
||||
} returns arrayOf(mockk())
|
||||
|
||||
every {
|
||||
mockAndroidKeyStore.setKeyEntry(
|
||||
expectedAlias,
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
)
|
||||
} throws KeyStoreException()
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.KeyStoreOperationFailed,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `importMutualTlsCertificate should return DuplicateAlias when alias already exists in AndroidKeyStore`() {
|
||||
setupMockAndroidKeyStore()
|
||||
setupMockPkcs12KeyStore()
|
||||
val expectedAlias = "mockAlias"
|
||||
val pkcs12Bytes = "key.p12".toByteArray()
|
||||
val password = "password"
|
||||
|
||||
every { mockPkcs12KeyStore.aliases() } returns mockk {
|
||||
every { hasMoreElements() } returns true
|
||||
every { nextElement() } returns "mockInternalAlias"
|
||||
}
|
||||
|
||||
every {
|
||||
mockPkcs12KeyStore.getKey(
|
||||
"mockInternalAlias",
|
||||
password.toCharArray(),
|
||||
)
|
||||
} returns mockk()
|
||||
every {
|
||||
mockPkcs12KeyStore.getCertificateChain("mockInternalAlias")
|
||||
} returns arrayOf(mockk())
|
||||
|
||||
every { mockAndroidKeyStore.containsAlias(expectedAlias) } returns true
|
||||
|
||||
assertEquals(
|
||||
ImportPrivateKeyResult.Error.DuplicateAlias,
|
||||
keyDiskSource.importMutualTlsCertificate(
|
||||
key = pkcs12Bytes,
|
||||
alias = expectedAlias,
|
||||
password = password,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupMockAndroidKeyStore() {
|
||||
every { KeyStore.getInstance("AndroidKeyStore") } returns mockAndroidKeyStore
|
||||
every { mockAndroidKeyStore.load(null) } just runs
|
||||
}
|
||||
|
||||
private fun setupMockPkcs12KeyStore() {
|
||||
every { KeyStore.getInstance("pkcs12") } returns mockPkcs12KeyStore
|
||||
every { mockPkcs12KeyStore.load(any(), any()) } just runs
|
||||
}
|
||||
}
|
|
@ -245,17 +245,17 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `remember me should be toggled on or off according to the state`() {
|
||||
composeTestRule.onNodeWithText("Remember me").assertIsOff()
|
||||
composeTestRule.onNodeWithText("Remember email").assertIsOff()
|
||||
|
||||
mutableStateFlow.update { it.copy(isRememberMeEnabled = true) }
|
||||
mutableStateFlow.update { it.copy(isRememberEmailEnabled = true) }
|
||||
|
||||
composeTestRule.onNodeWithText("Remember me").assertIsOn()
|
||||
composeTestRule.onNodeWithText("Remember email").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remember me click should send RememberMeToggle action`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Remember me")
|
||||
.onNodeWithText("Remember email")
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LandingAction.RememberMeToggle(true))
|
||||
|
@ -264,7 +264,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `create account click should send CreateAccountClick action`() {
|
||||
composeTestRule.onNodeWithText("Create account").performScrollTo().performClick()
|
||||
composeTestRule.onNodeWithText("Create an account").performScrollTo().performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
||||
}
|
||||
|
@ -473,7 +473,7 @@ private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
|||
private val DEFAULT_STATE = LandingState(
|
||||
emailInput = "",
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = false,
|
||||
isRememberEmailEnabled = false,
|
||||
selectedEnvironmentType = Environment.Type.US,
|
||||
selectedEnvironmentLabel = Environment.Us.label,
|
||||
dialog = null,
|
||||
|
|
|
@ -60,7 +60,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
DEFAULT_STATE.copy(
|
||||
emailInput = rememberedEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEmailEnabled = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -107,7 +107,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
val expectedState = DEFAULT_STATE.copy(
|
||||
emailInput = "test",
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEmailEnabled = true,
|
||||
)
|
||||
val handle = SavedStateHandle(mapOf("state" to expectedState))
|
||||
val viewModel = createViewModel(savedStateHandle = handle)
|
||||
|
@ -242,7 +242,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
val initialState = DEFAULT_STATE.copy(
|
||||
emailInput = rememberedEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEmailEnabled = true,
|
||||
accountSummaries = accountSummaries,
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -298,7 +298,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
val initialState = DEFAULT_STATE.copy(
|
||||
emailInput = rememberedEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEmailEnabled = true,
|
||||
accountSummaries = accountSummaries,
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -359,7 +359,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
val initialState = DEFAULT_STATE.copy(
|
||||
emailInput = rememberedEmail,
|
||||
isContinueButtonEnabled = true,
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEmailEnabled = true,
|
||||
accountSummaries = accountSummaries,
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -434,7 +434,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
viewModel.trySendAction(LandingAction.RememberMeToggle(true))
|
||||
assertEquals(
|
||||
viewModel.stateFlow.value,
|
||||
DEFAULT_STATE.copy(isRememberMeEnabled = true),
|
||||
DEFAULT_STATE.copy(isRememberEmailEnabled = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -599,7 +599,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
private val DEFAULT_STATE = LandingState(
|
||||
emailInput = "",
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberMeEnabled = false,
|
||||
isRememberEmailEnabled = false,
|
||||
selectedEnvironmentType = Environment.Type.US,
|
||||
selectedEnvironmentLabel = Environment.Us.label,
|
||||
dialog = null,
|
||||
|
|
|
@ -155,7 +155,7 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `remember me click should send RememberMeToggle action`() {
|
||||
composeTestRule.onNodeWithText("Remember me").performClick()
|
||||
composeTestRule.onNodeWithText("Remember").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(TwoFactorLoginAction.RememberMeToggle(true))
|
||||
}
|
||||
|
@ -163,11 +163,11 @@ class TwoFactorLoginScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `remember me should be toggled on or off according to the state`() {
|
||||
composeTestRule.onNodeWithText("Remember me").assertIsOff()
|
||||
composeTestRule.onNodeWithText("Remember").assertIsOff()
|
||||
|
||||
mutableStateFlow.update { it.copy(isRememberMeEnabled = true) }
|
||||
mutableStateFlow.update { it.copy(isRememberEnabled = true) }
|
||||
|
||||
composeTestRule.onNodeWithText("Remember me").assertIsOn()
|
||||
composeTestRule.onNodeWithText("Remember").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -290,7 +290,7 @@ private val DEFAULT_STATE = TwoFactorLoginState(
|
|||
displayEmail = "ex***@email.com",
|
||||
dialogState = null,
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberMeEnabled = false,
|
||||
isRememberEnabled = false,
|
||||
captchaToken = null,
|
||||
email = "example@email.com",
|
||||
password = "password123",
|
||||
|
|
|
@ -118,7 +118,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
twoFactorData = TwoFactorDataModel(
|
||||
code = token,
|
||||
method = TwoFactorAuthMethod.YUBI_KEY.value.toString(),
|
||||
remember = DEFAULT_STATE.isRememberMeEnabled,
|
||||
remember = DEFAULT_STATE.isRememberEnabled,
|
||||
),
|
||||
captchaToken = DEFAULT_STATE.captchaToken,
|
||||
orgIdentifier = DEFAULT_STATE.orgIdentifier,
|
||||
|
@ -786,7 +786,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
|||
viewModel.trySendAction(TwoFactorLoginAction.RememberMeToggle(true))
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
isRememberMeEnabled = true,
|
||||
isRememberEnabled = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -1045,7 +1045,7 @@ private val DEFAULT_STATE = TwoFactorLoginState(
|
|||
displayEmail = "ex***@email.com",
|
||||
dialogState = null,
|
||||
isContinueButtonEnabled = false,
|
||||
isRememberMeEnabled = false,
|
||||
isRememberEnabled = false,
|
||||
captchaToken = null,
|
||||
email = DEFAULT_EMAIL_ADDRESS,
|
||||
password = DEFAULT_PASSWORD,
|
||||
|
|
Loading…
Add table
Reference in a new issue