Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class AuthenticatorBridgeRepositoryImpl(
}
}

@Suppress("LongMethod")
override suspend fun getSharedAccounts(): SharedAccountData {
return authDiskSource
.userState
Expand Down Expand Up @@ -88,7 +89,7 @@ class AuthenticatorBridgeRepositoryImpl(
}

// Vault is unlocked, query vault disk source for totp logins:
val totpUris = vaultDiskSource
val cipherData = vaultDiskSource
.getTotpCiphers(userId = userId)
// Filter out any deleted and archived ciphers.
.filter { it.deletedDate == null && it.archivedDate == null }
Expand All @@ -97,10 +98,23 @@ class AuthenticatorBridgeRepositoryImpl(
.decryptCipher(userId = userId, cipher = it.toEncryptedSdkCipher())
.getOrNull()
?.let { decryptedCipher ->
val rawTotp = decryptedCipher.login?.totp
val cipherId = decryptedCipher.id ?: return@let null
val cipherName = decryptedCipher.name
val username = decryptedCipher.login?.username
rawTotp.sanitizeTotpUri(issuer = cipherName, username = username)
decryptedCipher.login?.totp?.let { rawTotp ->
SharedAccountData.CipherData(
uri = rawTotp,
// TODO: PM-34085 Remove the legacyUri.
legacyUri = rawTotp.sanitizeTotpUri(
issuer = cipherName,
username = username,
),
id = cipherId,
name = cipherName,
username = username,
isFavorite = decryptedCipher.favorite,
)
}
}
}

Expand All @@ -116,7 +130,7 @@ class AuthenticatorBridgeRepositoryImpl(
.environmentUrlData
.toEnvironmentUrlsOrDefault()
.label,
totpUris = totpUris,
cipherData = cipherData,
)
}
.let(::SharedAccountData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,33 +540,55 @@ private val USER_1_ENCRYPTED_SDK_TOTP_CIPHER = mockk<Cipher>()
private val USER_2_ENCRYPTED_SDK_TOTP_CIPHER = mockk<Cipher>()

private val USER_1_DECRYPTED_TOTP_CIPHER = mockk<CipherView> {
every { id } returns "id1"
every { login?.totp } returns "totp"
every { login?.username } returns "username"
every { name } returns "cipher1"
every { favorite } returns true
}
private val USER_2_DECRYPTED_TOTP_CIPHER = mockk<CipherView> {
every { id } returns "id2"
every { login?.totp } returns "totp"
every { login?.username } returns "username"
every { name } returns "cipher1"
every { favorite } returns false
}

private val USER_1_EXPECTED_TOTP_LIST = listOf("totp")
private val USER_2_EXPECTED_TOTP_LIST = listOf("totp")
private val USER_1_EXPECTED_CIPHER_LIST = listOf(
SharedAccountData.CipherData(
uri = "totp",
legacyUri = "totp",
id = "id1",
name = "cipher1",
username = "username",
isFavorite = true,
),
)
private val USER_2_EXPECTED_CIPHER_LIST = listOf(
SharedAccountData.CipherData(
uri = "totp",
legacyUri = "totp",
id = "id2",
name = "cipher1",
username = "username",
isFavorite = false,
),
)

private val USER_1_SHARED_ACCOUNT = SharedAccountData.Account(
userId = ACCOUNT_JSON_1.profile.userId,
name = ACCOUNT_JSON_1.profile.name,
email = ACCOUNT_JSON_1.profile.email,
environmentLabel = Environment.Us.label,
totpUris = USER_1_EXPECTED_TOTP_LIST,
cipherData = USER_1_EXPECTED_CIPHER_LIST,
)

private val USER_2_SHARED_ACCOUNT = SharedAccountData.Account(
userId = ACCOUNT_JSON_2.profile.userId,
name = ACCOUNT_JSON_2.profile.name,
email = ACCOUNT_JSON_2.profile.email,
environmentLabel = Environment.Us.label,
totpUris = USER_2_EXPECTED_TOTP_LIST,
cipherData = USER_2_EXPECTED_CIPHER_LIST,
)

private val USER_1_CIPHERS = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,23 @@ import com.bitwarden.authenticatorbridge.model.SharedAccountData
*/
fun List<SharedAccountData.Account>.toAuthenticatorItems(): List<AuthenticatorItem> =
flatMap { sharedAccount ->
sharedAccount.totpUris.mapNotNull { totpUriString ->
sharedAccount.cipherData.mapNotNull { cipherData ->
runCatching {
val uri = totpUriString.toUri()
val issuer = uri.getQueryParameter(TotpCodeManager.ISSUER_PARAM)
val label = uri.pathSegments
val uri = cipherData.uri.toUri()
val issuer = uri
.getQueryParameter(TotpCodeManager.ISSUER_PARAM)
?.takeUnless { it.isBlank() }
?: cipherData.name.takeUnless {
// TODO: PM-34085 The cipher name will never be blank once we
// TODO: remove the legacy support.
it.isBlank()
}
val label = uri
.pathSegments
.firstOrNull()
?.removePrefix("$issuer:")
?.takeUnless { it.isBlank() }
?: cipherData.username

AuthenticatorItem(
source = AuthenticatorItem.Source.Shared(
Expand All @@ -25,7 +35,7 @@ fun List<SharedAccountData.Account>.toAuthenticatorItems(): List<AuthenticatorIt
email = sharedAccount.email,
environmentLabel = sharedAccount.environmentLabel,
),
otpUri = totpUriString,
otpUri = cipherData.uri,
issuer = issuer,
label = label,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ private fun ItemListingContent(

SharedCodesDisplayState.Error -> {
item(key = "shared_codes_error") {
Spacer(modifier = Modifier.height(height = 8.dp))
Text(
text = stringResource(BitwardenString.shared_codes_error),
color = BitwardenTheme.colorScheme.text.secondary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.net.Uri
import app.cash.turbine.test
import com.bitwarden.authenticator.data.authenticator.datasource.disk.util.FakeAuthenticatorDiskSource
import com.bitwarden.authenticator.data.authenticator.datasource.entity.createMockAuthenticatorItemEntity
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.authenticator.data.authenticator.manager.TotpCodeManager
import com.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
import com.bitwarden.authenticator.data.authenticator.repository.model.AuthenticatorItem
Expand All @@ -25,6 +24,7 @@ import com.bitwarden.authenticatorbridge.model.SharedAccountData
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.util.mockBuilder
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.ui.platform.model.FileData
import io.mockk.coEvery
import io.mockk.coVerify
Expand Down Expand Up @@ -182,7 +182,7 @@ class AuthenticatorRepositoryTest {
name = null,
email = "test@test.com",
environmentLabel = "bitwarden.com",
totpUris = emptyList(),
cipherData = emptyList(),
),
)
authenticatorRepository.firstTimeAccountSyncFlow.test {
Expand All @@ -203,7 +203,7 @@ class AuthenticatorRepositoryTest {
name = null,
email = "test@test.com",
environmentLabel = "bitwarden.com",
totpUris = emptyList(),
cipherData = emptyList(),
),
)
authenticatorRepository.firstTimeAccountSyncFlow.test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,33 @@ data class SharedAccountData(
* @param name name associated with the account.
* @param email email associated with the account.
* @param environmentLabel environment associated with the account.
* @param totpUris list of totp URIs associated with the account.
* @param lastSyncTime the last time the account was synced by the main Bitwarden app.
Copy link
Contributor

@andrebispo5 andrebispo5 Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catchπŸ‘

* @param cipherData list of ciphers containing totp URIs associated with the account.
*/
data class Account(
val userId: String,
val name: String?,
val email: String,
val environmentLabel: String,
val totpUris: List<String>,
val cipherData: List<CipherData>,
)

/**
* Models a single shared cipher containing a totp.
*
* @param uri the totp URI.
* @param legacyUri the legacy totp URI.
* @param id unique ID for this item.
* @param name the name of the cipher.
* @param username the username of the item.
* @param isFavorite indicates that this item is a favorite.
*/
data class CipherData constructor(
val uri: String,
// TODO: PM-34085 Remove the legacyUri.
val legacyUri: String?,
val id: String,
val name: String,
val username: String?,
val isFavorite: Boolean,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
*
* For domain level model, see [SharedAccountData].
*
* @param accounts The list of shared accounts.
* @property accounts The list of shared accounts.
*/
@Serializable
internal data class SharedAccountDataJson(
Expand All @@ -19,12 +19,13 @@ internal data class SharedAccountDataJson(
/**
* Models a single shared account in a serializable format.
*
* @param userId user ID tied to the account.
* @param name name associated with the account.
* @param email email associated with the account.
* @param environmentLabel environment associated with the account.
* @param totpUris list of totp URIs associated with the account.
* @param lastSyncTime the last time the account was synced by the main Bitwarden app.
* @property userId user ID tied to the account.
* @property name name associated with the account.
* @property email email associated with the account.
* @property environmentLabel environment associated with the account.
* @property totpUris list of totp URIs associated with the account. This is for legacy use
* only.
* @property cipherData list of ciphers containing totp URIs associated with the account.
*/
@Serializable
data class AccountJson(
Expand All @@ -40,7 +41,39 @@ internal data class SharedAccountDataJson(
@SerialName("environmentLabel")
val environmentLabel: String,

// TODO: PM-34085 Remove totpUris.
@SerialName("totpUris")
val totpUris: List<String>,

// TODO: PM-34085 Make cipherData nonnull.
@SerialName("cipherData")
val cipherData: List<CipherJson>?,
)

/**
* Models a single shared cipher in a serializable format.
*
* @property uri the totp URI associated with this cipher.
* @property id the ID of this cipher.
* @property name the name of this cipher.
* @property username the username for this cipher.
* @property isFavorite indicates if this cipher is favorited.
*/
@Serializable
data class CipherJson(
@SerialName("uri")
val uri: String,

@SerialName("id")
val id: String,

@SerialName("cipherName")
val name: String,

@SerialName("username")
val username: String?,

@SerialName("isFavorite")
val isFavorite: Boolean,
)
}
Loading
Loading