mirror of
https://github.com/bitwarden/android.git
synced 2026-08-24 14:28:58 -05:00
BWA-238: bug: Send additional cipher data for Authenticator Sync (#6714)
This commit is contained in:
+18
-4
@@ -48,6 +48,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override suspend fun getSharedAccounts(): SharedAccountData {
|
||||
return authDiskSource
|
||||
.userState
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +130,7 @@ class AuthenticatorBridgeRepositoryImpl(
|
||||
.environmentUrlData
|
||||
.toEnvironmentUrlsOrDefault()
|
||||
.label,
|
||||
totpUris = totpUris,
|
||||
cipherData = cipherData,
|
||||
)
|
||||
}
|
||||
.let(::SharedAccountData)
|
||||
|
||||
+26
-4
@@ -540,25 +540,47 @@ 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(
|
||||
@@ -566,7 +588,7 @@ private val USER_2_SHARED_ACCOUNT = SharedAccountData.Account(
|
||||
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(
|
||||
|
||||
+15
-5
@@ -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(
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+3
-3
@@ -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
|
||||
@@ -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
|
||||
@@ -182,7 +182,7 @@ class AuthenticatorRepositoryTest {
|
||||
name = null,
|
||||
email = "test@test.com",
|
||||
environmentLabel = "bitwarden.com",
|
||||
totpUris = emptyList(),
|
||||
cipherData = emptyList(),
|
||||
),
|
||||
)
|
||||
authenticatorRepository.firstTimeAccountSyncFlow.test {
|
||||
@@ -203,7 +203,7 @@ class AuthenticatorRepositoryTest {
|
||||
name = null,
|
||||
email = "test@test.com",
|
||||
environmentLabel = "bitwarden.com",
|
||||
totpUris = emptyList(),
|
||||
cipherData = emptyList(),
|
||||
),
|
||||
)
|
||||
authenticatorRepository.firstTimeAccountSyncFlow.test {
|
||||
|
||||
+22
-3
@@ -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.
|
||||
* @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,
|
||||
)
|
||||
}
|
||||
|
||||
+40
-7
@@ -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(
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+53
-6
@@ -19,6 +19,8 @@ import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Generate a symmetric [SecretKey] that will used for encrypting IPC traffic.
|
||||
@@ -167,37 +169,82 @@ private fun generateCipher(): Cipher =
|
||||
/**
|
||||
* Helper function for converting [SharedAccountData] to a serializable [SharedAccountDataJson].
|
||||
*/
|
||||
private fun SharedAccountData.toJsonModel() = SharedAccountDataJson(
|
||||
private fun SharedAccountData.toJsonModel(): SharedAccountDataJson = SharedAccountDataJson(
|
||||
accounts = this.accounts.map { account ->
|
||||
SharedAccountDataJson.AccountJson(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
environmentLabel = account.environmentLabel,
|
||||
email = account.email,
|
||||
totpUris = account.totpUris,
|
||||
// TODO: PM-34085 Remove totpUris from this model.
|
||||
totpUris = account.cipherData.mapNotNull { it.legacyUri },
|
||||
cipherData = account.cipherData.map { it.toJsonModel() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper function for converting [SharedAccountData.CipherData] to a
|
||||
* [SharedAccountDataJson.CipherJson].
|
||||
*/
|
||||
private fun SharedAccountData.CipherData.toJsonModel(): SharedAccountDataJson.CipherJson =
|
||||
SharedAccountDataJson.CipherJson(
|
||||
uri = this.uri,
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
username = this.username,
|
||||
isFavorite = this.isFavorite,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper function for converting [SharedAccountDataJson] to a [SharedAccountData].
|
||||
*/
|
||||
private fun SharedAccountDataJson.toDomainModel() = SharedAccountData(
|
||||
private fun SharedAccountDataJson.toDomainModel(): SharedAccountData = SharedAccountData(
|
||||
accounts = this.accounts.map { account ->
|
||||
SharedAccountData.Account(
|
||||
userId = account.userId,
|
||||
name = account.name,
|
||||
environmentLabel = account.environmentLabel,
|
||||
email = account.email,
|
||||
totpUris = account.totpUris,
|
||||
cipherData = account.cipherData?.map { it.toCipherData() }
|
||||
// TODO: PM-34085 Remove this mapping from totpUris.
|
||||
?: account.totpUris.map { it.toCipherData() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper function for converting [SharedAccountDataJson.CipherJson] to a
|
||||
* [SharedAccountData.CipherData].
|
||||
*/
|
||||
private fun SharedAccountDataJson.CipherJson.toCipherData(): SharedAccountData.CipherData =
|
||||
SharedAccountData.CipherData(
|
||||
uri = this.uri,
|
||||
legacyUri = this.uri,
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
username = this.username,
|
||||
isFavorite = this.isFavorite,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper function for converting [String] URI to a [SharedAccountData.CipherData].
|
||||
* TODO: PM-34085 Remove this function, it is only needed for legacy support.
|
||||
*/
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
private fun String.toCipherData(): SharedAccountData.CipherData = SharedAccountData.CipherData(
|
||||
uri = this,
|
||||
legacyUri = this,
|
||||
id = Uuid.random().toString(),
|
||||
name = "",
|
||||
username = null,
|
||||
isFavorite = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Helper function for converting [AddTotpLoginItemDataJson] to a [AddTotpLoginItemData].
|
||||
*/
|
||||
private fun AddTotpLoginItemDataJson.toDomainModel() = AddTotpLoginItemData(
|
||||
private fun AddTotpLoginItemDataJson.toDomainModel(): AddTotpLoginItemData = AddTotpLoginItemData(
|
||||
totpUri = totpUri,
|
||||
)
|
||||
|
||||
@@ -205,6 +252,6 @@ private fun AddTotpLoginItemDataJson.toDomainModel() = AddTotpLoginItemData(
|
||||
* Helper function for converting [AddTotpLoginItemData] to a serializable
|
||||
* [AddTotpLoginItemDataJson].
|
||||
*/
|
||||
private fun AddTotpLoginItemData.toJsonModel() = AddTotpLoginItemDataJson(
|
||||
private fun AddTotpLoginItemData.toJsonModel(): AddTotpLoginItemDataJson = AddTotpLoginItemDataJson(
|
||||
totpUri = totpUri,
|
||||
)
|
||||
|
||||
+10
-1
@@ -173,7 +173,16 @@ private val SHARED_ACCOUNT_DATA = SharedAccountData(
|
||||
name = "Johnny Appleseed",
|
||||
email = "johnyapples@test.com",
|
||||
environmentLabel = "bitwarden.com",
|
||||
totpUris = listOf("test.com"),
|
||||
cipherData = listOf(
|
||||
SharedAccountData.CipherData(
|
||||
uri = "test.com",
|
||||
legacyUri = "test.com",
|
||||
id = "1234",
|
||||
name = "test",
|
||||
username = null,
|
||||
isFavorite = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user