1
0
Fork 0
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:
Andre Rosado 2025-01-27 11:55:38 +00:00
commit c18b5184c4
No known key found for this signature in database
GPG key ID: 99F68267CCD45AA9
22 changed files with 1224 additions and 110 deletions

View file

@ -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()
}
}

View file

@ -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}")
}
}
?: ""
}

View file

@ -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,
}

View file

@ -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?
}

View file

@ -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"

View file

@ -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(

View file

@ -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())
}
}

View file

@ -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.
*/

View file

@ -253,9 +253,9 @@ private fun LoginScreenContent(
modifier = Modifier.testTag("GetMasterPasswordHintLabel"),
)
},
passwordFieldTestTag = "MasterPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.testTag("MasterPasswordEntry")
.standardHorizontalMargin()
.fillMaxWidth(),
)

View file

@ -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"),

View file

@ -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 = "",

View file

@ -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,

View file

@ -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 = {},
)
}
}

View file

@ -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 {

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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",

View file

@ -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,