I just wanted to add a single property...

This commit is contained in:
Patrick Honkonen 2024-10-22 18:45:00 -04:00
parent 7aab846244
commit eea032d0a7
No known key found for this signature in database
GPG key ID: B63AF42A5531C877
32 changed files with 792 additions and 9 deletions

View file

@ -31,6 +31,7 @@ sealed class FlagKey<out T : Any> {
OnboardingFlow, OnboardingFlow,
OnboardingCarousel, OnboardingCarousel,
ImportLoginsFlow, ImportLoginsFlow,
SshKeyCipherItems,
) )
} }
} }
@ -107,4 +108,10 @@ sealed class FlagKey<out T : Any> {
override val defaultValue: String = "defaultValue" override val defaultValue: String = "defaultValue"
override val isRemotelyConfigured: Boolean = true override val isRemotelyConfigured: Boolean = true
} }
data object SshKeyCipherItems : FlagKey<Boolean>() {
override val keyName: String = "ssh-key-cipher-items"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}
} }

View file

@ -45,6 +45,9 @@ val CipherView.subtitle: String?
} }
} }
} }
// TODO: Return SSH key subtitle (PM-10405)
CipherType.SSH_KEY -> null
} }
/** /**

View file

@ -94,6 +94,7 @@ class VaultDiskSourceImpl(
shouldHidePasswords = collection.shouldHidePasswords, shouldHidePasswords = collection.shouldHidePasswords,
externalId = collection.externalId, externalId = collection.externalId,
isReadOnly = collection.isReadOnly, isReadOnly = collection.isReadOnly,
canManage = collection.canManage,
), ),
) )
} }
@ -114,6 +115,7 @@ class VaultDiskSourceImpl(
shouldHidePasswords = entity.shouldHidePasswords, shouldHidePasswords = entity.shouldHidePasswords,
externalId = entity.externalId, externalId = entity.externalId,
isReadOnly = entity.isReadOnly, isReadOnly = entity.isReadOnly,
canManage = entity.canManage,
) )
} }
}, },
@ -229,6 +231,7 @@ class VaultDiskSourceImpl(
shouldHidePasswords = collection.shouldHidePasswords, shouldHidePasswords = collection.shouldHidePasswords,
externalId = collection.externalId, externalId = collection.externalId,
isReadOnly = collection.isReadOnly, isReadOnly = collection.isReadOnly,
canManage = collection.canManage,
) )
}, },
) )

View file

@ -26,7 +26,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
FolderEntity::class, FolderEntity::class,
SendEntity::class, SendEntity::class,
], ],
version = 3, version = 4,
exportSchema = true, exportSchema = true,
) )
@TypeConverters(ZonedDateTimeTypeConverter::class) @TypeConverters(ZonedDateTimeTypeConverter::class)

View file

@ -30,4 +30,7 @@ data class CollectionEntity(
@ColumnInfo(name = "read_only") @ColumnInfo(name = "read_only")
val isReadOnly: Boolean, val isReadOnly: Boolean,
@ColumnInfo(name = "manage")
val canManage: Boolean,
) )

View file

@ -51,6 +51,9 @@ data class CipherJsonRequest(
@SerialName("secureNote") @SerialName("secureNote")
val secureNote: SyncResponseJson.Cipher.SecureNote?, val secureNote: SyncResponseJson.Cipher.SecureNote?,
@SerialName("sshKey")
val sshKey: SyncResponseJson.Cipher.SshKey?,
@SerialName("folderId") @SerialName("folderId")
val folderId: String?, val folderId: String?,

View file

@ -33,6 +33,9 @@ enum class CipherTypeJson {
*/ */
@SerialName("4") @SerialName("4")
IDENTITY, IDENTITY,
@SerialName("5")
SSH_KEY,
} }
@Keep @Keep

View file

@ -718,6 +718,16 @@ data class SyncResponseJson(
) )
} }
@Serializable
data class SshKey(
@SerialName("publicKey")
val publicKey: String?,
@SerialName("privateKey")
val privateKey: String?,
@SerialName("keyFingerprint")
val fingerprint: String?,
)
/** /**
* Represents password history in the vault response. * Represents password history in the vault response.
* *
@ -927,6 +937,7 @@ data class SyncResponseJson(
* @property externalId The external ID of the collection (nullable). * @property externalId The external ID of the collection (nullable).
* @property isReadOnly If the collection is marked as read only. * @property isReadOnly If the collection is marked as read only.
* @property id The ID of the collection. * @property id The ID of the collection.
* @property canManage If the collection can be managed.
*/ */
@Serializable @Serializable
data class Collection( data class Collection(
@ -947,5 +958,8 @@ data class SyncResponseJson(
@SerialName("id") @SerialName("id")
val id: String, val id: String,
@SerialName("manage")
val canManage: Boolean,
) )
} }

View file

@ -10,10 +10,12 @@ import com.bitwarden.sdk.Fido2CredentialStore
import com.bitwarden.send.Send import com.bitwarden.send.Send
import com.bitwarden.send.SendType import com.bitwarden.send.SendType
import com.bitwarden.send.SendView import com.bitwarden.send.SendView
import com.bitwarden.vault.CipherRepromptType
import com.bitwarden.vault.CipherType import com.bitwarden.vault.CipherType
import com.bitwarden.vault.CipherView import com.bitwarden.vault.CipherView
import com.bitwarden.vault.CollectionView import com.bitwarden.vault.CollectionView
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.bitwarden.vault.SshKeyView
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
@ -21,8 +23,10 @@ import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherDeleteData
import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData import com.x8bit.bitwarden.data.platform.manager.model.SyncCipherUpsertData
import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData import com.x8bit.bitwarden.data.platform.manager.model.SyncFolderDeleteData
@ -113,6 +117,7 @@ import kotlinx.coroutines.launch
import retrofit2.HttpException import retrofit2.HttpException
import java.time.Clock import java.time.Clock
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.UUID
/** /**
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the * A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
@ -138,6 +143,7 @@ class VaultRepositoryImpl(
private val vaultLockManager: VaultLockManager, private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
private val userLogoutManager: UserLogoutManager, private val userLogoutManager: UserLogoutManager,
private val featureFlagManager: FeatureFlagManager,
pushManager: PushManager, pushManager: PushManager,
private val clock: Clock, private val clock: Clock,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -176,15 +182,52 @@ class VaultRepositoryImpl(
foldersStateFlow, foldersStateFlow,
collectionsStateFlow, collectionsStateFlow,
sendDataStateFlow, sendDataStateFlow,
) { ciphersDataState, foldersDataState, collectionsDataState, sendsDataState -> featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems),
) { ciphersDataState, foldersDataState, collectionsDataState, sendsDataState, sshKeyCipherItemsEnabled ->
combineDataStates( combineDataStates(
ciphersDataState, ciphersDataState,
foldersDataState, foldersDataState,
collectionsDataState, collectionsDataState,
sendsDataState, sendsDataState,
) { ciphersData, foldersData, collectionsData, sendsData -> ) { ciphersData, foldersData, collectionsData, sendsData ->
//{"Cipher":{"reprompt":0,"lastKnownRevisionDate":"2024-10-22T22:05:29.167Z","type":5,"sshKey":{"publicKey":"2.Rt3CPEUQCkax35wOV9dErA==|nocAOVCjoi0hh4PZwXOEIg==|h3WkP+gDNixsiNxAWMBqqBEwpZJRRORB+eX8DNo+EPU=","privateKey":"2.DRnXPdhLeiC14NoRjzmpQg==|a+L20fnV4tpmPm/+l6JEYQ==|CMc0MbQTXkTOayHEeL1G06H30DDOIic3l8wjUARgNIA=","keyFingerprint":"2.MiBuM5TNmWCiMufi3imBTw==|R6R6+dXw8CS5BV5Gk4plAg==|cupnpiK6rCbYaLLVXR8RqA/Iv+PJyIvPgJPwbwEQjrA="},"name":"2.0FJqtenxtVG8zh0AvVbwYA==|wU4BOVzSmEwEf5td1HuJ5g==|0K5wEQ/tMM7PlxBsz5EsZT0tP5fJSj/0hG5SYfUlTq4=","fields":[],"favorite":false,"key":"2.7XaYYaz2ONX/XmESHLj9Ag==|oAtEvfWpU9/47e7L+M4ob3/2D8I6z+0PXdiset/n+QUsCgEIgR3t3M7sQ3sYbH9Ui43Gb1NRNBLqx72Vo5JccfptTrjD2JJ1mffW3ZKNCTc=|hPQP4lKhrmDxwUz6yxwtp9/F2B5eL0aUWqSkDDb+vzY="},"CollectionIds":[]}
val mutableCipherStateData = ciphersData.toMutableList()
mutableCipherStateData.add(
CipherView(
id = UUID.randomUUID().toString(),
organizationId = null,
folderId = null,
collectionIds = emptyList(),
key = "2.7XaYYaz2ONX/XmESHLj9Ag==|oAtEvfWpU9/47e7L+M4ob3/2D8I6z+0PXdiset/n+QUsCgEIgR3t3M7sQ3sYbH9Ui43Gb1NRNBLqx72Vo5JccfptTrjD2JJ1mffW3ZKNCTc=|hPQP4lKhrmDxwUz6yxwtp9/F2B5eL0aUWqSkDDb+vzY=",
name = "Unlocks doorz",
notes = null,
type = CipherType.SSH_KEY,
login = null,
identity = null,
card = null,
secureNote = null,
sshKey = SshKeyView(
publicKey = "publicKey",
privateKey = "privateKey",
fingerprint = "fingerprint",
),
favorite = false,
reprompt = CipherRepromptType.NONE,
organizationUseTotp = false,
edit = true,
viewPassword = true,
localData = null,
attachments = emptyList(),
fields = null,
passwordHistory = null,
creationDate = DateTime.parse("2024-10-22T22:05:29.167Z"),
deletedDate = null,
revisionDate = DateTime.parse("2024-10-22T22:05:29.167Z"),
),
)
VaultData( VaultData(
cipherViewList = ciphersData, cipherViewList = mutableCipherStateData
.filterSshKeyCiphersIfNecessary(sshKeyCipherItemsEnabled),
fido2CredentialAutofillViewList = null, fido2CredentialAutofillViewList = null,
folderViewList = foldersData, folderViewList = foldersData,
collectionViewList = collectionsData, collectionViewList = collectionsData,
@ -926,6 +969,8 @@ class VaultRepositoryImpl(
) )
} }
//{"Cipher":{"reprompt":0,"lastKnownRevisionDate":"2024-10-22T21:15:02.098Z","type":5,"sshKey":{"publicKey":"2.e4Hv/B+xZqgB0LMRfL5Oow==|LbSjpCAxZyOVpiAb+xdIqQ==|MSDMmwQ5rrbtxru4hFFKrzdbHLNiRUpQh8/xX7//2bw=","privateKey":"2./CmD8GAJ4DU5hOCm8GnJMA==|qcJcSSonXLQQcci0ION4/g==|EwXB1bx0yQkY86RW9N2bmrtswC6+cdt6P7wrw+lyR8c=","keyFingerprint":"2.mx/OnN5fJMses2/HG/Q4ag==|cdsSFuXfD6Q8nQ+1sP8F7Q==|tMMUQ0jAGgYwbaEnqm9oMpIL2r1rDBtxDIIt6GN3dto="},"name":"2.TS++PBphUVR5Hrb4p2Vpiw==|hX5D0ZU3sZ455im8nSKxQg==|DvMxpDCbkDtL9TMNJDOt8fuYF1q69Apxke6s08u72JY=","fields":[],"favorite":false,"key":"2.ff7G7cYSv/8CH902tK0iFg==|2Jtmaylw6H6wQezZQepMTNie+jlmSQOWJIScx6I96AFCIP2yX29kLn9XCbFwTLblwkgVDGAGPofz7n7LDk6jY4NxjaiUqEhm/HZJ29oSP1Y=|T/Mj/aIZbW9YovgkTnUrpmCKcW/uwFKH1lHYJd0J3KA="},"CollectionIds":[]}
private fun observeVaultDiskCiphers( private fun observeVaultDiskCiphers(
userId: String, userId: String,
): Flow<DataState<List<CipherView>>> = ): Flow<DataState<List<CipherView>>> =
@ -947,6 +992,13 @@ class VaultRepositoryImpl(
.map { it.orLoadingIfNotSynced(userId = userId) } .map { it.orLoadingIfNotSynced(userId = userId) }
.onEach { mutableCiphersStateFlow.value = it } .onEach { mutableCiphersStateFlow.value = it }
private fun List<CipherView>.filterSshKeyCiphersIfNecessary(enabled: Boolean): List<CipherView> =
if (!enabled) {
filter { it.type != CipherType.SSH_KEY }
} else {
this
}
private fun observeVaultDiskDomains( private fun observeVaultDiskDomains(
userId: String, userId: String,
): Flow<DataState<DomainsData>> = ): Flow<DataState<DomainsData>> =

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.vault.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
@ -49,6 +50,7 @@ object VaultRepositoryModule {
totpCodeManager: TotpCodeManager, totpCodeManager: TotpCodeManager,
pushManager: PushManager, pushManager: PushManager,
userLogoutManager: UserLogoutManager, userLogoutManager: UserLogoutManager,
featureFlagManager: FeatureFlagManager,
clock: Clock, clock: Clock,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
syncService = syncService, syncService = syncService,
@ -66,6 +68,7 @@ object VaultRepositoryModule {
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
pushManager = pushManager, pushManager = pushManager,
userLogoutManager = userLogoutManager, userLogoutManager = userLogoutManager,
featureFlagManager = featureFlagManager,
clock = clock, clock = clock,
) )
} }

View file

@ -17,6 +17,7 @@ import com.bitwarden.vault.LoginUri
import com.bitwarden.vault.PasswordHistory import com.bitwarden.vault.PasswordHistory
import com.bitwarden.vault.SecureNote import com.bitwarden.vault.SecureNote
import com.bitwarden.vault.SecureNoteType import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SshKey
import com.bitwarden.vault.UriMatchType import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator import com.x8bit.bitwarden.data.platform.util.SpecialCharWithPrecedenceComparator
import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.AttachmentJsonRequest
@ -55,6 +56,7 @@ fun Cipher.toEncryptedNetworkCipher(): CipherJsonRequest =
isFavorite = favorite, isFavorite = favorite,
card = card?.toEncryptedNetworkCard(), card = card?.toEncryptedNetworkCard(),
key = key, key = key,
sshKey = sshKey?.toEncryptedNetworkSshKey(),
) )
/** /**
@ -102,6 +104,13 @@ private fun Card.toEncryptedNetworkCard(): SyncResponseJson.Cipher.Card =
brand = brand, brand = brand,
) )
private fun SshKey.toEncryptedNetworkSshKey(): SyncResponseJson.Cipher.SshKey =
SyncResponseJson.Cipher.SshKey(
publicKey = publicKey,
privateKey = privateKey,
fingerprint = fingerprint,
)
/** /**
* Converts a list of Bitwarden SDK [Field] objects to a corresponding * Converts a list of Bitwarden SDK [Field] objects to a corresponding
* list of [SyncResponseJson.Cipher.Field] objects. * list of [SyncResponseJson.Cipher.Field] objects.
@ -309,6 +318,7 @@ private fun CipherType.toNetworkCipherType(): CipherTypeJson =
CipherType.SECURE_NOTE -> CipherTypeJson.SECURE_NOTE CipherType.SECURE_NOTE -> CipherTypeJson.SECURE_NOTE
CipherType.CARD -> CipherTypeJson.CARD CipherType.CARD -> CipherTypeJson.CARD
CipherType.IDENTITY -> CipherTypeJson.IDENTITY CipherType.IDENTITY -> CipherTypeJson.IDENTITY
CipherType.SSH_KEY -> CipherTypeJson.SSH_KEY
} }
/** /**
@ -334,6 +344,8 @@ fun SyncResponseJson.Cipher.toEncryptedSdkCipher(): Cipher =
type = type.toSdkCipherType(), type = type.toSdkCipherType(),
login = login?.toSdkLogin(), login = login?.toSdkLogin(),
identity = identity?.toSdkIdentity(), identity = identity?.toSdkIdentity(),
// TODO: Support SSH key (PM-10405)
sshKey = null,
card = card?.toSdkCard(), card = card?.toSdkCard(),
secureNote = secureNote?.toSdkSecureNote(), secureNote = secureNote?.toSdkSecureNote(),
favorite = isFavorite, favorite = isFavorite,
@ -517,6 +529,7 @@ fun CipherTypeJson.toSdkCipherType(): CipherType =
CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE CipherTypeJson.SECURE_NOTE -> CipherType.SECURE_NOTE
CipherTypeJson.CARD -> CipherType.CARD CipherTypeJson.CARD -> CipherType.CARD
CipherTypeJson.IDENTITY -> CipherType.IDENTITY CipherTypeJson.IDENTITY -> CipherType.IDENTITY
CipherTypeJson.SSH_KEY -> CipherType.SSH_KEY
} }
/** /**

View file

@ -17,6 +17,7 @@ fun SyncResponseJson.Collection.toEncryptedSdkCollection(): Collection =
externalId = this.externalId, externalId = this.externalId,
hidePasswords = this.shouldHidePasswords, hidePasswords = this.shouldHidePasswords,
readOnly = this.isReadOnly, readOnly = this.isReadOnly,
manage = this.canManage,
) )
/** /**

View file

@ -27,6 +27,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.OnboardingCarousel, FlagKey.OnboardingCarousel,
FlagKey.OnboardingFlow, FlagKey.OnboardingFlow,
FlagKey.ImportLoginsFlow, FlagKey.ImportLoginsFlow,
FlagKey.SshKeyCipherItems,
-> BooleanFlagItem( -> BooleanFlagItem(
label = flagKey.getDisplayLabel(), label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>, key = flagKey as FlagKey<Boolean>,
@ -69,4 +70,5 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel) FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel)
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow) FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
FlagKey.ImportLoginsFlow -> stringResource(R.string.import_logins_flow) FlagKey.ImportLoginsFlow -> stringResource(R.string.import_logins_flow)
FlagKey.SshKeyCipherItems -> stringResource(R.string.ssh_key_cipher_item_types)
} }

View file

@ -247,6 +247,7 @@ private val CipherType.iconRes: Int
CipherType.SECURE_NOTE -> R.drawable.ic_note CipherType.SECURE_NOTE -> R.drawable.ic_note
CipherType.CARD -> R.drawable.ic_payment_card CipherType.CARD -> R.drawable.ic_payment_card
CipherType.IDENTITY -> R.drawable.ic_id_card CipherType.IDENTITY -> R.drawable.ic_id_card
CipherType.SSH_KEY -> R.drawable.ic_ssh_key
} }
/** /**

View file

@ -31,11 +31,13 @@ import kotlinx.collections.immutable.toImmutableList
fun VaultAddEditContent( fun VaultAddEditContent(
state: VaultAddEditState.ViewState.Content, state: VaultAddEditState.ViewState.Content,
isAddItemMode: Boolean, isAddItemMode: Boolean,
typeOptions: List<VaultAddEditState.ItemTypeOption>,
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit, onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
commonTypeHandlers: VaultAddEditCommonHandlers, commonTypeHandlers: VaultAddEditCommonHandlers,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers, loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers, identityItemTypeHandlers: VaultAddEditIdentityTypeHandlers,
cardItemTypeHandlers: VaultAddEditCardTypeHandlers, cardItemTypeHandlers: VaultAddEditCardTypeHandlers,
sshKeyItemTypeHandlers: VaultAddEditSshKeyTypeHandlers,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
permissionsManager: PermissionsManager, permissionsManager: PermissionsManager,
) { ) {
@ -45,6 +47,7 @@ fun VaultAddEditContent(
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit is VaultAddEditState.ViewState.Content.ItemType.Card -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit is VaultAddEditState.ViewState.Content.ItemType.Identity -> Unit
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> Unit
is VaultAddEditState.ViewState.Content.ItemType.Login -> { is VaultAddEditState.ViewState.Content.ItemType.Login -> {
loginItemTypeHandlers.onSetupTotpClick(isGranted) loginItemTypeHandlers.onSetupTotpClick(isGranted)
} }
@ -77,6 +80,7 @@ fun VaultAddEditContent(
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
TypeOptionsItem( TypeOptionsItem(
entries = typeOptions,
itemType = state.type, itemType = state.type,
onTypeOptionClicked = onTypeOptionClicked, onTypeOptionClicked = onTypeOptionClicked,
modifier = Modifier modifier = Modifier
@ -131,6 +135,15 @@ fun VaultAddEditContent(
commonTypeHandlers = commonTypeHandlers, commonTypeHandlers = commonTypeHandlers,
) )
} }
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> {
vaultAddEditSshKeyItems(
commonState = state.common,
sshKeyState = state.type,
commonTypeHandlers = commonTypeHandlers,
sshKeyTypeHandlers = sshKeyItemTypeHandlers,
)
}
} }
item { item {
@ -141,12 +154,12 @@ fun VaultAddEditContent(
@Composable @Composable
private fun TypeOptionsItem( private fun TypeOptionsItem(
entries: List<VaultAddEditState.ItemTypeOption>,
itemType: VaultAddEditState.ViewState.Content.ItemType, itemType: VaultAddEditState.ViewState.Content.ItemType,
onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit, onTypeOptionClicked: (VaultAddEditState.ItemTypeOption) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val possibleMainStates = VaultAddEditState.ItemTypeOption.entries.toList() val optionsWithStrings = entries.associateWith { stringResource(id = it.labelRes) }
val optionsWithStrings = possibleMainStates.associateWith { stringResource(id = it.labelRes) }
BitwardenMultiSelectButton( BitwardenMultiSelectButton(
label = stringResource(id = R.string.type), label = stringResource(id = R.string.type),

View file

@ -155,6 +155,10 @@ fun VaultAddEditScreen(
VaultAddEditCardTypeHandlers.create(viewModel = viewModel) VaultAddEditCardTypeHandlers.create(viewModel = viewModel)
} }
val sshKeyItemTypeHandlers = remember(viewModel) {
VaultAddEditSshKeyTypeHandlers.create(viewModel = viewModel)
}
val confirmDeleteClickAction = remember(viewModel) { val confirmDeleteClickAction = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.ConfirmDeleteClick) } { viewModel.trySendAction(VaultAddEditAction.Common.ConfirmDeleteClick) }
} }
@ -321,6 +325,7 @@ fun VaultAddEditScreen(
VaultAddEditContent( VaultAddEditContent(
state = viewState, state = viewState,
isAddItemMode = state.isAddItemMode, isAddItemMode = state.isAddItemMode,
typeOptions = state.supportedItemTypes,
onTypeOptionClicked = remember(viewModel) { onTypeOptionClicked = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.TypeOptionSelect(it)) } { viewModel.trySendAction(VaultAddEditAction.Common.TypeOptionSelect(it)) }
}, },
@ -329,6 +334,7 @@ fun VaultAddEditScreen(
permissionsManager = permissionsManager, permissionsManager = permissionsManager,
identityItemTypeHandlers = identityItemTypeHandlers, identityItemTypeHandlers = identityItemTypeHandlers,
cardItemTypeHandlers = cardItemTypeHandlers, cardItemTypeHandlers = cardItemTypeHandlers,
sshKeyItemTypeHandlers = sshKeyItemTypeHandlers,
modifier = Modifier modifier = Modifier
.imePadding() .imePadding()
.padding(innerPadding) .padding(innerPadding)

View file

@ -0,0 +1,137 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
fun LazyListScope.vaultAddEditSshKeyItems(
commonState: VaultAddEditState.ViewState.Content.Common,
sshKeyState: VaultAddEditState.ViewState.Content.ItemType.SshKey,
commonTypeHandlers: VaultAddEditCommonHandlers,
sshKeyTypeHandlers: VaultAddEditSshKeyTypeHandlers,
) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = commonTypeHandlers.onNameTextChange,
modifier = Modifier
.testTag("ItemNameEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.public_key),
value = sshKeyState.publicKey,
onValueChange = sshKeyTypeHandlers.onPublicKeyTextChange,
showPassword = sshKeyState.showPublicKey,
showPasswordChange = { sshKeyTypeHandlers.onPublicKeyVisibilityChange(it) },
showPasswordTestTag = "ViewPublicKeyButton",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.private_key),
value = sshKeyState.privateKey,
onValueChange = sshKeyTypeHandlers.onPrivateKeyTextChange,
showPassword = sshKeyState.showPrivateKey,
showPasswordChange = { sshKeyTypeHandlers.onPrivateKeyVisibilityChange(it) },
showPasswordTestTag = "ViewPrivateKeyButton",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.fingerprint),
value = sshKeyState.fingerprint,
onValueChange = sshKeyTypeHandlers.onFingerprintTextChange,
showPassword = sshKeyState.showFingerprint,
showPasswordChange = { sshKeyTypeHandlers.onFingerprintVisibilityChange(it) },
showPasswordTestTag = "ViewFingerprintButton",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
data class VaultAddEditSshKeyTypeHandlers(
val onPublicKeyTextChange: (String) -> Unit,
val onPublicKeyVisibilityChange: (Boolean) -> Unit,
val onPrivateKeyTextChange: (String) -> Unit,
val onPrivateKeyVisibilityChange: (Boolean) -> Unit,
val onFingerprintTextChange: (String) -> Unit,
val onFingerprintVisibilityChange: (Boolean) -> Unit,
) {
companion object {
fun create(viewModel: VaultAddEditViewModel): VaultAddEditSshKeyTypeHandlers =
VaultAddEditSshKeyTypeHandlers(
onPublicKeyTextChange = { newPublicKey ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange(
publicKey = newPublicKey,
),
)
},
onPublicKeyVisibilityChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.PublicKeyVisibilityChange(
isVisible = it
),
)
},
onPrivateKeyTextChange = { newPrivateKey ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange(
privateKey = newPrivateKey,
),
)
},
onPrivateKeyVisibilityChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange(
isVisible = it
),
)
},
onFingerprintTextChange = { newFingerprint ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange(
fingerprint = newFingerprint,
),
)
},
onFingerprintVisibilityChange = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.SshKeyType.FingerprintVisibilityChange(
isVisible = it
),
)
},
)
}
}

View file

@ -16,10 +16,12 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
@ -101,6 +103,7 @@ class VaultAddEditViewModel @Inject constructor(
private val resourceManager: ResourceManager, private val resourceManager: ResourceManager,
private val clock: Clock, private val clock: Clock,
private val organizationEventManager: OrganizationEventManager, private val organizationEventManager: OrganizationEventManager,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>( ) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE]
@ -136,18 +139,31 @@ class VaultAddEditViewModel @Inject constructor(
null null
} }
val supportedItemTypes =
if (featureFlagManager.getFeatureFlag(key = FlagKey.SshKeyCipherItems)) {
VaultAddEditState.ItemTypeOption.entries.toList()
} else {
VaultAddEditState.ItemTypeOption.entries.filterNot {
it == VaultAddEditState.ItemTypeOption.SSH_KEYS
}
}
VaultAddEditState( VaultAddEditState(
vaultAddEditType = vaultAddEditType, vaultAddEditType = vaultAddEditType,
viewState = when (vaultAddEditType) { viewState = when (vaultAddEditType) {
is VaultAddEditType.AddItem -> { is VaultAddEditType.AddItem -> {
autofillSelectionData autofillSelectionData
?.toDefaultAddTypeContent(isIndividualVaultDisabled) ?.toDefaultAddTypeContent(isIndividualVaultDisabled)
?: autofillSaveItem?.toDefaultAddTypeContent(isIndividualVaultDisabled) ?: autofillSaveItem?.toDefaultAddTypeContent(
isIndividualVaultDisabled
)
?: fido2CreationRequest?.toDefaultAddTypeContent( ?: fido2CreationRequest?.toDefaultAddTypeContent(
attestationOptions = fido2AttestationOptions, attestationOptions = fido2AttestationOptions,
isIndividualVaultDisabled = isIndividualVaultDisabled, isIndividualVaultDisabled = isIndividualVaultDisabled,
) )
?: totpData?.toDefaultAddTypeContent(isIndividualVaultDisabled) ?: totpData?.toDefaultAddTypeContent(
isIndividualVaultDisabled
)
?: VaultAddEditState.ViewState.Content( ?: VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(), common = VaultAddEditState.ViewState.Content.Common(),
isIndividualVaultDisabled = isIndividualVaultDisabled, isIndividualVaultDisabled = isIndividualVaultDisabled,
@ -163,6 +179,7 @@ class VaultAddEditViewModel @Inject constructor(
// Set special conditions for autofill and fido2 save // Set special conditions for autofill and fido2 save
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null, shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
shouldExitOnSave = shouldExitOnSave, shouldExitOnSave = shouldExitOnSave,
supportedItemTypes = supportedItemTypes,
) )
}, },
) { ) {
@ -204,6 +221,11 @@ class VaultAddEditViewModel @Inject constructor(
} }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
featureFlagManager.getFeatureFlagFlow(FlagKey.SshKeyCipherItems)
.map { VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
override fun handleAction(action: VaultAddEditAction) { override fun handleAction(action: VaultAddEditAction) {
@ -212,6 +234,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action) is VaultAddEditAction.ItemType.LoginType -> handleAddLoginTypeAction(action)
is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action) is VaultAddEditAction.ItemType.IdentityType -> handleIdentityTypeActions(action)
is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action) is VaultAddEditAction.ItemType.CardType -> handleCardTypeActions(action)
is VaultAddEditAction.ItemType.SshKeyType -> handleSshKeyTypeActions(action)
is VaultAddEditAction.Internal -> handleInternalActions(action) is VaultAddEditAction.Internal -> handleInternalActions(action)
} }
} }
@ -321,6 +344,7 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditState.ItemTypeOption.CARD -> handleSwitchToAddCardItem() VaultAddEditState.ItemTypeOption.CARD -> handleSwitchToAddCardItem()
VaultAddEditState.ItemTypeOption.IDENTITY -> handleSwitchToAddIdentityItem() VaultAddEditState.ItemTypeOption.IDENTITY -> handleSwitchToAddIdentityItem()
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> handleSwitchToAddSecureNotesItem() VaultAddEditState.ItemTypeOption.SECURE_NOTES -> handleSwitchToAddSecureNotesItem()
VaultAddEditState.ItemTypeOption.SSH_KEYS -> handleSwitchToSshKeyItem()
} }
} }
@ -372,6 +396,18 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
private fun handleSwitchToSshKeyItem() {
updateContent { currentContent ->
currentContent.copy(
common = currentContent.clearNonSharedData(),
type = currentContent.previousItemTypeOrDefault(
itemType = VaultAddEditState.ItemTypeOption.SSH_KEYS,
),
previousItemTypes = currentContent.toUpdatedPreviousItemTypes(),
)
}
}
@Suppress("LongMethod") @Suppress("LongMethod")
private fun handleSaveClick() = onContent { content -> private fun handleSaveClick() = onContent { content ->
if (content.common.name.isBlank()) { if (content.common.name.isBlank()) {
@ -1364,6 +1400,64 @@ class VaultAddEditViewModel @Inject constructor(
//endregion Card Type Handlers //endregion Card Type Handlers
//region SSH Key Type Handlers
private fun handleSshKeyTypeActions(action: VaultAddEditAction.ItemType.SshKeyType) {
when (action) {
is VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange -> {
handlePublicKeyTextChange(action)
}
is VaultAddEditAction.ItemType.SshKeyType.PublicKeyVisibilityChange -> {
handlePublicKeyVisibilityChange(action)
}
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange -> {
handlePrivateKeyTextChange(action)
}
is VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange -> {
handlePrivateKeyVisibilityChange(action)
}
is VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange -> {
handleSshKeyFingerprintTextChange(action)
}
is VaultAddEditAction.ItemType.SshKeyType.FingerprintVisibilityChange -> {
handleSshKeyFingerprintVisibilityChange(action)
}
}
}
private fun handlePublicKeyTextChange(action: VaultAddEditAction.ItemType.SshKeyType.PublicKeyTextChange) {
updateSshKeyContent { it.copy(publicKey = action.publicKey) }
}
private fun handlePublicKeyVisibilityChange(action: VaultAddEditAction.ItemType.SshKeyType.PublicKeyVisibilityChange) {
updateSshKeyContent { it.copy(showPublicKey = action.isVisible) }
}
private fun handlePrivateKeyTextChange(action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyTextChange) {
updateSshKeyContent { it.copy(privateKey = action.privateKey) }
}
private fun handlePrivateKeyVisibilityChange(action: VaultAddEditAction.ItemType.SshKeyType.PrivateKeyVisibilityChange) {
updateSshKeyContent { it.copy(showPrivateKey = action.isVisible) }
}
private fun handleSshKeyFingerprintTextChange(action: VaultAddEditAction.ItemType.SshKeyType.FingerprintTextChange) {
updateSshKeyContent { it.copy(fingerprint = action.fingerprint) }
}
private fun handleSshKeyFingerprintVisibilityChange(
action: VaultAddEditAction.ItemType.SshKeyType.FingerprintVisibilityChange,
) {
updateSshKeyContent { it.copy(showFingerprint = action.isVisible) }
}
//endregion SSH Key Type Handlers
//region Internal Type Handlers //region Internal Type Handlers
private fun handleInternalActions(action: VaultAddEditAction.Internal) { private fun handleInternalActions(action: VaultAddEditAction.Internal) {
@ -1398,6 +1492,10 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> { is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> {
handleValidateFido2PinResultReceive(action) handleValidateFido2PinResultReceive(action)
} }
is VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive -> {
handleSshKeyCipherItemsFeatureFlagReceive(action)
}
} }
} }
@ -1708,6 +1806,22 @@ class VaultAddEditViewModel @Inject constructor(
getRequestAndRegisterCredential() getRequestAndRegisterCredential()
} }
private fun handleSshKeyCipherItemsFeatureFlagReceive(
action: VaultAddEditAction.Internal.SshKeyCipherItemsFeatureFlagReceive,
) {
mutableStateFlow.update {
it.copy(
supportedItemTypes = if (action.enabled) {
VaultAddEditState.ItemTypeOption.entries
} else {
VaultAddEditState.ItemTypeOption.entries.filterNot {
it == VaultAddEditState.ItemTypeOption.SSH_KEYS
}
}
)
}
}
//endregion Internal Type Handlers //endregion Internal Type Handlers
//region Utility Functions //region Utility Functions
@ -1819,6 +1933,19 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
private inline fun updateSshKeyContent(
crossinline block: (VaultAddEditState.ViewState.Content.ItemType.SshKey) ->
VaultAddEditState.ViewState.Content.ItemType.SshKey,
) {
updateContent { currentContent ->
(currentContent.type as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
currentContent.copy(
type = block(it),
)
}
}
}
private fun VaultAddEditState.ViewState.Content.clearNonSharedData(): private fun VaultAddEditState.ViewState.Content.clearNonSharedData():
VaultAddEditState.ViewState.Content.Common = VaultAddEditState.ViewState.Content.Common =
common.copy( common.copy(
@ -1853,6 +1980,10 @@ class VaultAddEditViewModel @Inject constructor(
VaultAddEditState.ItemTypeOption.SECURE_NOTES -> { VaultAddEditState.ItemTypeOption.SECURE_NOTES -> {
VaultAddEditState.ViewState.Content.ItemType.SecureNotes VaultAddEditState.ViewState.Content.ItemType.SecureNotes
} }
VaultAddEditState.ItemTypeOption.SSH_KEYS -> {
VaultAddEditState.ViewState.Content.ItemType.SshKey()
}
}, },
) )
@ -1915,6 +2046,7 @@ data class VaultAddEditState(
// Internal // Internal
val shouldExitOnSave: Boolean = false, val shouldExitOnSave: Boolean = false,
val totpData: TotpData? = null, val totpData: TotpData? = null,
val supportedItemTypes: List<ItemTypeOption>,
) : Parcelable { ) : Parcelable {
/** /**
@ -1958,6 +2090,7 @@ data class VaultAddEditState(
CARD(R.string.type_card), CARD(R.string.type_card),
IDENTITY(R.string.type_identity), IDENTITY(R.string.type_identity),
SECURE_NOTES(R.string.type_secure_note), SECURE_NOTES(R.string.type_secure_note),
SSH_KEYS(R.string.type_ssh_key),
} }
/** /**
@ -2163,6 +2296,25 @@ data class VaultAddEditState(
data object SecureNotes : ItemType() { data object SecureNotes : ItemType() {
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SECURE_NOTES
} }
/**
* Represents the `SshKey` item type.
*
* @property publicKey The public key for the SSH key item.
* @property privateKey The private key for the SSH key item.
* @property fingerprint The fingerprint for the SSH key item.
*/
@Parcelize
data class SshKey(
val publicKey: String = "",
val privateKey: String = "",
val fingerprint: String = "",
val showPublicKey: Boolean = false,
val showPrivateKey: Boolean = false,
val showFingerprint: Boolean = false,
) : ItemType() {
override val itemTypeOption: ItemTypeOption get() = ItemTypeOption.SSH_KEYS
}
} }
/** /**
@ -2929,6 +3081,15 @@ sealed class VaultAddEditAction {
*/ */
data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType() data class SecurityCodeVisibilityChange(val isVisible: Boolean) : CardType()
} }
sealed class SshKeyType : ItemType() {
data class PublicKeyTextChange(val publicKey: String) : SshKeyType()
data class PublicKeyVisibilityChange(val isVisible: Boolean) : SshKeyType()
data class PrivateKeyTextChange(val privateKey: String) : SshKeyType()
data class PrivateKeyVisibilityChange(val isVisible: Boolean) : SshKeyType()
data class FingerprintTextChange(val fingerprint: String) : SshKeyType()
data class FingerprintVisibilityChange(val isVisible: Boolean) : SshKeyType()
}
} }
/** /**
@ -2953,6 +3114,10 @@ sealed class VaultAddEditAction {
val generatorResult: GeneratorResult, val generatorResult: GeneratorResult,
) : Internal() ) : Internal()
data class SshKeyCipherItemsFeatureFlagReceive(
val enabled: Boolean,
) : Internal()
/** /**
* Indicates that the vault item data has been received. * Indicates that the vault item data has been received.
*/ */

View file

@ -66,4 +66,5 @@ private val VaultAddEditState.ViewState.Content.ItemType.defaultLinkedFieldTypeO
is VaultAddEditState.ViewState.Content.ItemType.Identity -> VaultLinkedFieldType.TITLE is VaultAddEditState.ViewState.Content.ItemType.Identity -> VaultLinkedFieldType.TITLE
is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME is VaultAddEditState.ViewState.Content.ItemType.Login -> VaultLinkedFieldType.USERNAME
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> null
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> null
} }

View file

@ -88,6 +88,12 @@ fun CipherView.toViewState(
zip = identity?.postalCode.orEmpty(), zip = identity?.postalCode.orEmpty(),
country = identity?.country.orEmpty(), country = identity?.country.orEmpty(),
) )
CipherType.SSH_KEY -> VaultAddEditState.ViewState.Content.ItemType.SshKey(
publicKey = sshKey?.publicKey.orEmpty(),
privateKey = sshKey?.privateKey.orEmpty(),
fingerprint = sshKey?.fingerprint.orEmpty(),
)
}, },
common = VaultAddEditState.ViewState.Content.Common( common = VaultAddEditState.ViewState.Content.Common(
originalCipher = this, originalCipher = this,

View file

@ -45,6 +45,7 @@ import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
/** /**
* Displays the vault item screen. * Displays the vault item screen.
@ -265,6 +266,9 @@ fun VaultItemScreen(
vaultCardItemTypeHandlers = remember(viewModel) { vaultCardItemTypeHandlers = remember(viewModel) {
VaultCardItemTypeHandlers.create(viewModel = viewModel) VaultCardItemTypeHandlers.create(viewModel = viewModel)
}, },
vaultSshKeyItemTypeHandlers = remember(viewModel) {
VaultSshKeyItemTypeHandlers.create(viewModel = viewModel)
},
) )
} }
} }
@ -342,6 +346,7 @@ private fun VaultItemContent(
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers, vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers, vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
vaultCardItemTypeHandlers: VaultCardItemTypeHandlers, vaultCardItemTypeHandlers: VaultCardItemTypeHandlers,
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
when (viewState) { when (viewState) {
@ -389,6 +394,15 @@ private fun VaultItemContent(
modifier = modifier, modifier = modifier,
) )
} }
is VaultItemState.ViewState.Content.ItemType.SshKey -> {
VaultItemSshKeyContent(
commonState = viewState.common,
sshKeyItemState = viewState.type,
vaultSshKeyItemTypeHandlers = vaultSshKeyItemTypeHandlers,
modifier = modifier,
)
}
} }
} }

View file

@ -0,0 +1,108 @@
package com.x8bit.bitwarden.ui.vault.feature.item
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
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.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
@Composable
fun VaultItemSshKeyContent(
commonState: VaultItemState.ViewState.Content.Common,
sshKeyItemState: VaultItemState.ViewState.Content.ItemType.SshKey,
vaultSshKeyItemTypeHandlers: VaultSshKeyItemTypeHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
BitwardenListHeaderText(
label = stringResource(id = R.string.item_information),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = commonState.name,
onValueChange = { },
readOnly = true,
singleLine = false,
modifier = Modifier
.testTag("SshKeyItemNameEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
sshKeyItemState.publicKey?.let { publicKey ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.public_key),
value = publicKey,
onValueChange = { },
singleLine = false,
readOnly = true,
showPassword = sshKeyItemState.showPublicKey,
showPasswordTestTag = "ViewPublicKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPublicKeyClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
sshKeyItemState.privateKey?.let { privateKey ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.private_key),
value = privateKey,
onValueChange = { },
singleLine = false,
readOnly = true,
showPassword = sshKeyItemState.showPrivateKey,
showPasswordTestTag = "ViewPrivateKeyButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowPrivateKeyClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
sshKeyItemState.fingerprint?.let { fingerprint ->
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(
label = stringResource(id = R.string.fingerprint),
value = fingerprint,
onValueChange = { },
singleLine = false,
readOnly = true,
showPassword = sshKeyItemState.showFingerprint,
showPasswordTestTag = "ViewFingerprintButton",
showPasswordChange = vaultSshKeyItemTypeHandlers.onShowFingerprintClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
}
}

View file

@ -116,6 +116,7 @@ class VaultItemViewModel @Inject constructor(
when (action) { when (action) {
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action) is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
is VaultItemAction.ItemType.Card -> handleCardTypeActions(action) is VaultItemAction.ItemType.Card -> handleCardTypeActions(action)
is VaultItemAction.ItemType.SshKey -> handleSshKeyTypeActions(action)
is VaultItemAction.Common -> handleCommonActions(action) is VaultItemAction.Common -> handleCommonActions(action)
is VaultItemAction.Internal -> handleInternalAction(action) is VaultItemAction.Internal -> handleInternalAction(action)
} }
@ -753,6 +754,64 @@ class VaultItemViewModel @Inject constructor(
//endregion Card Type Handlers //endregion Card Type Handlers
//region SSH Key Type Handlers
private fun handleSshKeyTypeActions(action: VaultItemAction.ItemType.SshKey) {
when (action) {
is VaultItemAction.ItemType.SshKey.PublicKeyVisibilityClicked -> {
handlePublicKeyVisibilityClicked(action)
}
is VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked -> {
handlePrivateKeyVisibilityClicked(action)
}
is VaultItemAction.ItemType.SshKey.FingerprintVisibilityClicked -> {
handleFingerprintVisibilityClicked(action)
}
}
}
private fun handlePublicKeyVisibilityClicked(action: VaultItemAction.ItemType.SshKey.PublicKeyVisibilityClicked) {
onSshKeyContent { content, sshKey ->
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = content.copy(
type = sshKey.copy(showPublicKey = action.isVisible),
),
)
}
}
}
private fun handlePrivateKeyVisibilityClicked(action: VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked) {
onSshKeyContent { content, sshKey ->
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = content.copy(
type = sshKey.copy(showPrivateKey = action.isVisible),
),
)
}
}
}
private fun handleFingerprintVisibilityClicked(
action: VaultItemAction.ItemType.SshKey.FingerprintVisibilityClicked,
) {
onSshKeyContent { content, sshKey ->
mutableStateFlow.update { currentState ->
currentState.copy(
viewState = content.copy(
type = sshKey.copy(showFingerprint = action.isVisible),
),
)
}
}
}
//endregion SSH Key Type Handlers
//region Internal Type Handlers //region Internal Type Handlers
private fun handleInternalAction(action: VaultItemAction.Internal) { private fun handleInternalAction(action: VaultItemAction.Internal) {
@ -1057,6 +1116,21 @@ class VaultItemViewModel @Inject constructor(
} }
} }
} }
private inline fun onSshKeyContent(
crossinline block: (
VaultItemState.ViewState.Content,
VaultItemState.ViewState.Content.ItemType.SshKey,
) -> Unit,
) {
state.viewState.asContentOrNull()
?.let { content ->
(content.type as? VaultItemState.ViewState.Content.ItemType.SshKey)
?.let { sshKeyContent ->
block(content, sshKeyContent)
}
}
}
} }
/** /**
@ -1359,6 +1433,24 @@ data class VaultItemState(
val isVisible: Boolean, val isVisible: Boolean,
) : Parcelable ) : Parcelable
} }
/**
* Represents the data for displaying an `SSHKey` item type.
*
* @property name The name of the key.
* @property publicKey The SSH public key.
* @property privateKey The SSH private key.
* @property fingerprint The fingerprint of the key.
*/
data class SshKey(
val name: String?,
val publicKey: String?,
val privateKey: String?,
val fingerprint: String?,
val showPublicKey: Boolean,
val showPrivateKey: Boolean,
val showFingerprint: Boolean,
) : ItemType()
} }
} }
@ -1697,6 +1789,12 @@ sealed class VaultItemAction {
*/ */
data class NumberVisibilityClick(val isVisible: Boolean) : Card() data class NumberVisibilityClick(val isVisible: Boolean) : Card()
} }
sealed class SshKey : ItemType() {
data class PublicKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
data class PrivateKeyVisibilityClicked(val isVisible: Boolean) : SshKey()
data class FingerprintVisibilityClicked(val isVisible: Boolean) : SshKey()
}
} }
/** /**

View file

@ -0,0 +1,38 @@
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
data class VaultSshKeyItemTypeHandlers(
val onShowPublicKeyClick: (isVisible: Boolean) -> Unit,
val onShowPrivateKeyClick: (isVisible: Boolean) -> Unit,
val onShowFingerprintClick: (isVisible: Boolean) -> Unit,
) {
companion object {
fun create(viewModel: VaultItemViewModel): VaultSshKeyItemTypeHandlers =
VaultSshKeyItemTypeHandlers(
onShowPublicKeyClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.PublicKeyVisibilityClicked(
isVisible = it
)
)
},
onShowPrivateKeyClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.PrivateKeyVisibilityClicked(
isVisible = it
)
)
},
onShowFingerprintClick = {
viewModel.trySendAction(
VaultItemAction.ItemType.SshKey.FingerprintVisibilityClicked(
isVisible = it
)
)
},
)
}
}

View file

@ -156,6 +156,25 @@ fun CipherView.toViewState(
address = identity?.identityAddress, address = identity?.identityAddress,
) )
} }
CipherType.SSH_KEY -> {
val sshKeyValues = requireNotNull(sshKey)
VaultItemState.ViewState.Content.ItemType.SshKey(
name = name,
publicKey = sshKeyValues.publicKey,
privateKey = sshKeyValues.privateKey,
fingerprint = sshKeyValues.fingerprint,
showPublicKey = (previousState?.type as?
VaultItemState.ViewState.Content.ItemType.SshKey)
?.showPublicKey == true,
showPrivateKey = (previousState?.type as?
VaultItemState.ViewState.Content.ItemType.SshKey)
?.showPrivateKey == true,
showFingerprint = (previousState?.type as?
VaultItemState.ViewState.Content.ItemType.SshKey)
?.showFingerprint == true,
)
}
}, },
) )

View file

@ -369,6 +369,7 @@ private fun CipherView.toIconTestTag(): String =
CipherType.SECURE_NOTE -> "SecureNoteCipherIcon" CipherType.SECURE_NOTE -> "SecureNoteCipherIcon"
CipherType.CARD -> "CardCipherIcon" CipherType.CARD -> "CardCipherIcon"
CipherType.IDENTITY -> "IdentityCipherIcon" CipherType.IDENTITY -> "IdentityCipherIcon"
CipherType.SSH_KEY -> "SshKeyCipherIcon"
} }
private fun CipherView.toIconData( private fun CipherView.toIconData(
@ -425,4 +426,5 @@ private val CipherType.iconRes: Int
CipherType.SECURE_NOTE -> R.drawable.ic_note CipherType.SECURE_NOTE -> R.drawable.ic_note
CipherType.CARD -> R.drawable.ic_payment_card CipherType.CARD -> R.drawable.ic_payment_card
CipherType.IDENTITY -> R.drawable.ic_id_card CipherType.IDENTITY -> R.drawable.ic_id_card
CipherType.SSH_KEY -> R.drawable.ic_ssh_key
} }

View file

@ -927,6 +927,30 @@ data class VaultState(
) : VaultItem() { ) : VaultItem() {
override val supportingLabel: Text? get() = null override val supportingLabel: Text? get() = null
} }
/**
* Represents a SSH key item within the vault, designed to store SSH keys.
*
* @property publicKey The public key associated with this SSH key item.
* @property privateKey The private key associated with this SSH key item.
* @property fingerprint The fingerprint associated with this SSH key item.
*/
@Parcelize
data class SshKey(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_ssh_key),
override val startIconTestTag: String = "SshKeyCipherIcon",
override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
override val shouldShowMasterPasswordReprompt: Boolean,
val publicKey: Text?,
val privateKey: Text?,
val fingerprint: Text?,
) : VaultItem() {
override val supportingLabel: Text? get() = null
}
} }
} }

View file

@ -15,6 +15,7 @@ import com.bitwarden.vault.LoginView
import com.bitwarden.vault.PasswordHistoryView import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView import com.bitwarden.vault.SecureNoteView
import com.bitwarden.vault.SshKeyView
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
@ -49,6 +50,7 @@ fun VaultAddEditState.ViewState.Content.toCipherView(): CipherView =
secureNote = type.toSecureNotesView(), secureNote = type.toSecureNotesView(),
login = type.toLoginView(common = common), login = type.toLoginView(common = common),
card = type.toCardView(), card = type.toCardView(),
sshKey = type.toSshKeyView(),
// Fields we always grab from the UI // Fields we always grab from the UI
name = common.name, name = common.name,
@ -66,6 +68,16 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toCipherType(): CipherT
is VaultAddEditState.ViewState.Content.ItemType.Identity -> CipherType.IDENTITY is VaultAddEditState.ViewState.Content.ItemType.Identity -> CipherType.IDENTITY
is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN is VaultAddEditState.ViewState.Content.ItemType.Login -> CipherType.LOGIN
is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE is VaultAddEditState.ViewState.Content.ItemType.SecureNotes -> CipherType.SECURE_NOTE
is VaultAddEditState.ViewState.Content.ItemType.SshKey -> CipherType.SSH_KEY
}
private fun VaultAddEditState.ViewState.Content.ItemType.toSshKeyView(): SshKeyView? =
(this as? VaultAddEditState.ViewState.Content.ItemType.SshKey)?.let {
SshKeyView(
publicKey = it.publicKey.orNullIfBlank(),
privateKey = it.privateKey.orNullIfBlank(),
fingerprint = it.fingerprint.orNullIfBlank(),
)
} }
private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? = private fun VaultAddEditState.ViewState.Content.ItemType.toCardView(): CardView? =

View file

@ -259,6 +259,20 @@ private fun CipherView.toVaultItemOrNull(
extraIconList = toLabelIcons(), extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD, shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
) )
CipherType.SSH_KEY -> VaultState.ViewState.VaultItem.SshKey(
id = id,
name = name.asText(),
publicKey = sshKey?.publicKey?.asText(),
privateKey = sshKey?.privateKey?.asText(),
fingerprint = sshKey?.fingerprint?.asText(),
overflowOptions = toOverflowActions(
hasMasterPassword = hasMasterPassword,
isPremiumUser = isPremiumUser,
),
extraIconList = toLabelIcons(),
shouldShowMasterPasswordReprompt = reprompt == CipherRepromptType.PASSWORD,
)
} }
} }

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M14.838,11.797L15.439,11.916C16.826,12.191 18.316,11.79 19.387,10.719C21.096,9.01 21.096,6.24 19.387,4.531C17.679,2.823 14.909,2.823 13.2,4.531C12.128,5.603 11.728,7.092 12.003,8.48L12.136,9.153L4.375,16.574L3.601,19.67H6.024L6.649,17.17H8.524L9.149,14.67H11.533L14.838,11.797ZM12,15.92H10.125L9.5,18.42H7.625L7,20.92H2L3.25,15.92L10.776,8.723C10.424,6.943 10.937,5.027 12.316,3.648C14.513,1.451 18.075,1.451 20.271,3.648C22.468,5.844 22.468,9.406 20.271,11.602C18.892,12.981 16.975,13.495 15.196,13.142L12,15.92Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M16.736,6.299C16.492,6.543 16.492,6.939 16.736,7.183C16.98,7.427 17.376,7.427 17.62,7.183C17.864,6.939 17.864,6.543 17.62,6.299C17.376,6.055 16.98,6.055 16.736,6.299ZM18.504,5.415C19.236,6.148 19.236,7.335 18.504,8.067C17.771,8.799 16.584,8.799 15.852,8.067C15.12,7.335 15.12,6.148 15.852,5.415C16.584,4.683 17.771,4.683 18.504,5.415Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -1061,4 +1061,8 @@ Do you want to switch to this account?</string>
<string name="save_the_exported_file_highlight">Save the exported file</string> <string name="save_the_exported_file_highlight">Save the exported file</string>
<string name="this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server">This is not a recognized Bitwarden server. You may need to check with your provider or update your server.</string> <string name="this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server">This is not a recognized Bitwarden server. You may need to check with your provider or update your server.</string>
<string name="syncing_logins_loading_message">Syncing logins...</string> <string name="syncing_logins_loading_message">Syncing logins...</string>
<string name="type_ssh_key">SSH Key</string>
<string name="public_key">Public key</string>
<string name="private_key">Private key</string>
<string name="ssh_key_cipher_item_types">SSH Key Cipher Item Types</string>
</resources> </resources>

View file

@ -24,7 +24,7 @@ androidxSplash = "1.1.0-rc01"
androidXAppCompat = "1.7.0" androidXAppCompat = "1.7.0"
androdixAutofill = "1.1.0" androdixAutofill = "1.1.0"
androidxWork = "2.9.1" androidxWork = "2.9.1"
bitwardenSdk = "1.0.0-20240924.112512-21" bitwardenSdk = "1.0.0-20241022.103047-2"
crashlytics = "3.0.2" crashlytics = "3.0.2"
detekt = "1.23.7" detekt = "1.23.7"
firebaseBom = "33.4.0" firebaseBom = "33.4.0"
@ -83,7 +83,7 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidXSecurityCrypto" } androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidXSecurityCrypto" }
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" }
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" } bitwarden-sdk = { module = "com.bitwarden:sdk-android-temp", version.ref = "bitwardenSdk" }
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" } bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }
detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" } detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }