Skip to content
Open
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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk

import com.bitwarden.core.WrappedAccountCryptographicState
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.V2UpgradeTokenJson
import com.bitwarden.network.provider.AppIdProvider
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
Expand Down Expand Up @@ -410,4 +411,14 @@ interface AuthDiskSource : AppIdProvider {
* Stores the last lock timestamp for the given [userId].
*/
fun storeLastLockTimestamp(userId: String, lastLockTimestamp: Instant?)

/**
* Gets the v2 upgrade token for the given [userId].
*/
fun getV2UpgradeToken(userId: String): V2UpgradeTokenJson?

/**
* Stores the v2 upgrade token for the given [userId].
*/
fun storeV2UpgradeToken(userId: String, v2UpgradeToken: V2UpgradeTokenJson?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
import com.bitwarden.network.model.AccountKeysJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.V2UpgradeTokenJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
Expand Down Expand Up @@ -58,6 +59,7 @@ private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
private const val LAST_LOCK_TIMESTAMP = "lastLockTimestamp"
private const val PROFILE_ACCOUNT_KEYS_KEY = "profileAccountKeys"
private const val ACCOUNT_CRYPTOGRAPHIC_STATE_KEY = "accountCryptographicState"
private const val V2_UPGRADE_TOKEN = "v2UpgradeToken"

/**
* Primary implementation of [AuthDiskSource].
Expand Down Expand Up @@ -192,6 +194,7 @@ class AuthDiskSourceImpl(
userId = userId,
pinProtectedUserKeyEnvelope = null,
)
storeV2UpgradeToken(userId = userId, v2UpgradeToken = null)

// Certain values are never removed as required by the feature requirements:
// * DeviceKey
Expand Down Expand Up @@ -603,6 +606,20 @@ class AuthDiskSourceImpl(
)
}

override fun getV2UpgradeToken(
userId: String,
): V2UpgradeTokenJson? =
getString(key = V2_UPGRADE_TOKEN.appendIdentifier(identifier = userId))?.let {
json.decodeFromStringOrNull<V2UpgradeTokenJson>(string = it)
}

override fun storeV2UpgradeToken(userId: String, v2UpgradeToken: V2UpgradeTokenJson?) {
putString(
key = V2_UPGRADE_TOKEN.appendIdentifier(identifier = userId),
value = v2UpgradeToken?.let { json.encodeToString(value = it) },
)
}

private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import kotlinx.coroutines.runBlocking
/**
* Primary implementation of [SdkClientManager].
*/
class SdkClientManagerImpl(
internal class SdkClientManagerImpl(
nativeLibraryManager: NativeLibraryManager,
dispatcherManager: DispatcherManager,
sdkRepoFactory: SdkRepositoryFactory,
Expand All @@ -42,6 +42,9 @@ class SdkClientManagerImpl(
platform().state().registerClientManagedRepositories(
repositories = sdkRepoFactory.getRepositories(userId = userId),
)
userId?.let {
kmStateBridge().registerBridgeImpl(sdkRepoFactory.getStateBridge(userId = it))
}
}
},
) : SdkClientManager {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package com.x8bit.bitwarden.data.platform.manager.sdk

import com.bitwarden.core.ClientManagedTokens
import com.bitwarden.core.ClientSettings
import com.bitwarden.core.StateBridgeForeignImpl
import com.bitwarden.sdk.Repositories
import com.bitwarden.sdk.ServerCommunicationConfigRepository

/**
* Creates and manages sdk repositories.
*/
interface SdkRepositoryFactory {
/**
* Creates a [StateBridgeForeignImpl] for use with the Bitwarden SDK.
*/
fun getStateBridge(userId: String): StateBridgeForeignImpl

/**
* Retrieves or creates a [Repositories] for use with the Bitwarden SDK.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.manager.sdk
import com.bitwarden.core.ClientManagedTokens
import com.bitwarden.core.ClientSettings
import com.bitwarden.core.DeviceType
import com.bitwarden.core.StateBridgeForeignImpl
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.network.model.BitwardenServiceClientConfig
import com.bitwarden.sdk.Repositories
Expand All @@ -13,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkCipherReposit
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkLocalUserDataKeyStateRepository
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.SdkTokenRepository
import com.x8bit.bitwarden.data.platform.manager.sdk.repository.ServerCommunicationConfigRepositoryImpl
import com.x8bit.bitwarden.data.platform.manager.sdk.statebridge.SdkStateBridge
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource

/**
Expand All @@ -25,6 +27,14 @@ class SdkRepositoryFactoryImpl(
private val authDiskSource: AuthDiskSource,
private val serviceClientConfig: BitwardenServiceClientConfig,
) : SdkRepositoryFactory {
override fun getStateBridge(
userId: String,
): StateBridgeForeignImpl =
SdkStateBridge(
userId = userId,
authDiskSource = authDiskSource,
)

override fun getRepositories(userId: String?): Repositories =
Repositories(
cipher = getSdkCipherRepository(userId = userId),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.x8bit.bitwarden.data.platform.manager.sdk.statebridge

import com.bitwarden.core.MasterPasswordUnlockData
import com.bitwarden.core.StateBridgeForeignImpl
import com.bitwarden.core.V2UpgradeToken
import com.bitwarden.core.WrappedAccountCryptographicState
import com.bitwarden.crypto.EncString
import com.bitwarden.crypto.PasswordProtectedKeyEnvelope
import com.bitwarden.crypto.SymmetricCryptoKey
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.updateMasterPasswordUnlock
import com.x8bit.bitwarden.data.vault.repository.util.toSdkMasterPasswordUnlock
import com.x8bit.bitwarden.data.vault.repository.util.toV2UpgradeToken
import com.x8bit.bitwarden.data.vault.repository.util.toV2UpgradeTokenJson

/**
* A user-scoped implementation of a Bitwarden SDK [StateBridgeForeignImpl].
*/
@Suppress("TooManyFunctions")
internal class SdkStateBridge(
private val userId: String,
private val authDiskSource: AuthDiskSource,
) : StateBridgeForeignImpl {
@Volatile
private var inMemoryUserKey: SymmetricCryptoKey? = null

override suspend fun setUserKey(value: SymmetricCryptoKey) {
inMemoryUserKey = value
}

override suspend fun getUserKey(): SymmetricCryptoKey? = inMemoryUserKey

override suspend fun clearUserKey() {
inMemoryUserKey = null
}

override suspend fun setPersistentPinEnvelope(value: PasswordProtectedKeyEnvelope) {
authDiskSource.storePersistentPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = value,
)
}

override suspend fun getPersistentPinEnvelope(): PasswordProtectedKeyEnvelope? =
authDiskSource.getPersistentPinProtectedUserKeyEnvelope(userId = userId)

override suspend fun clearPersistentPinEnvelope() {
authDiskSource.storePersistentPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
)
}

override suspend fun setEphemeralPinEnvelope(value: PasswordProtectedKeyEnvelope) {
authDiskSource.storeEphemeralPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = value,
)
}

override suspend fun getEphemeralPinEnvelope(): PasswordProtectedKeyEnvelope? =
authDiskSource.getEphemeralPinProtectedUserKeyEnvelope(userId = userId)

override suspend fun clearEphemeralPinEnvelope() {
authDiskSource.storeEphemeralPinProtectedUserKeyEnvelope(
userId = userId,
pinProtectedUserKeyEnvelope = null,
)
}

override suspend fun setEncryptedPin(value: EncString) {
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = value)
}

override suspend fun getEncryptedPin(): EncString? =
authDiskSource.getEncryptedPin(userId = userId)

override suspend fun clearEncryptedPin() {
authDiskSource.storeEncryptedPin(userId = userId, encryptedPin = null)
}

override suspend fun setV2UpgradeToken(value: V2UpgradeToken) {
authDiskSource.storeV2UpgradeToken(
userId = userId,
v2UpgradeToken = value.toV2UpgradeTokenJson(),
)
}

override suspend fun getV2UpgradeToken(): V2UpgradeToken? =
authDiskSource
.getV2UpgradeToken(userId = userId)
?.toV2UpgradeToken()

override suspend fun clearV2UpgradeToken() {
authDiskSource.storeV2UpgradeToken(userId = userId, v2UpgradeToken = null)
}

override suspend fun setAccountCryptographicState(value: WrappedAccountCryptographicState) {
authDiskSource.storeAccountCryptographicState(
userId = userId,
accountCryptographicState = value,
)
}

override suspend fun getAccountCryptographicState(): WrappedAccountCryptographicState? =
authDiskSource.getAccountCryptographicState(userId = userId)

override suspend fun clearAccountCryptographicState() {
authDiskSource.storeAccountCryptographicState(
userId = userId,
accountCryptographicState = null,
)
}

override suspend fun setMasterpasswordUnlockData(value: MasterPasswordUnlockData) {
authDiskSource.userState = authDiskSource.userState?.updateMasterPasswordUnlock(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we have a updateMasterPasswordUnlock function here, why not also make a getMasterPasswordUnlock, instead of having the log . chain in the get below?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Setting the data is a more complicated process that we want abstracted for reusability, fetching the data is not particularly complicated, so we never bothered to abstract it.

For whatever it's worth, I am working on migrating the master password unlock data out of the userState. Once that work is done, they with both be simple get and set functions on the authDiskSource.

userId = userId,
masterPasswordUnlock = value,
)
}
Comment on lines +115 to +120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IMPORTANT: updateMasterPasswordUnlock also clears forcePasswordResetReason, so an SDK-driven unlock-data write silently drops a pending forced password reset.

Details and fix

UserStateJson.updateMasterPasswordUnlock unconditionally sets forcePasswordResetReason = null (UserStateJsonExtensions.kt:125). That is intended for its existing callers in AuthRepositoryImpl (set/change/remove password), but through this bridge the SDK can write unlock data while a reset is still pending β€” for example when the V2 upgrade re-wraps the user key during unlock for a user carrying ADMIN_FORCE_PASSWORD_RESET or WEAK_MASTER_PASSWORD_ON_LOGIN.

Nothing restores the flag afterwards: on sync, getForcePasswordResetReason only carries the previous value forward (?: previousForcePasswordResetReason), so needsPasswordReset stays false until the next login and the user is never prompted to reset.

Consider a helper that updates only masterPasswordUnlock/hasMasterPassword and leaves forcePasswordResetReason untouched for the bridge path.

Related: clearMasterpasswordUnlockData flips hasMasterPassword to false. Worth confirming the SDK only calls it when the master password is genuinely being removed, since the app uses that flag to route users into the set-password and TDE flows.


override suspend fun getMasterpasswordUnlockData(): MasterPasswordUnlockData? =
authDiskSource
.userState
?.accounts[userId]
?.profile
?.userDecryptionOptions
?.masterPasswordUnlock
?.toSdkMasterPasswordUnlock()

override suspend fun clearMasterpasswordUnlockData() {
authDiskSource.userState = authDiskSource.userState?.updateMasterPasswordUnlock(
userId = userId,
masterPasswordUnlock = null,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.logTag
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
import com.x8bit.bitwarden.data.vault.repository.util.toV2UpgradeToken
import com.x8bit.bitwarden.data.vault.repository.util.toVaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.update
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -192,7 +193,9 @@ class VaultLockManagerImpl(
email = email,
method = initUserCryptoMethod,
userId = userId,
upgradeToken = null,
upgradeToken = authDiskSource
.getV2UpgradeToken(userId = userId)
?.toV2UpgradeToken(),
),
)
.flatMap { result ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ class VaultSyncManagerImpl(
authDiskSource.userState = authDiskSource.userState?.toUpdatedUserStateJson(
syncResponse = syncResponse,
)
authDiskSource.storeV2UpgradeToken(
userId = userId,
v2UpgradeToken = syncResponse.userDecryption?.v2UpgradeToken,
)

unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
storeProfileData(syncResponse = syncResponse)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.vault.repository.util

import com.bitwarden.core.V2UpgradeToken
import com.bitwarden.network.model.V2UpgradeTokenJson

/**
* Converts the [V2UpgradeToken] into a [V2UpgradeTokenJson].
*/
fun V2UpgradeToken.toV2UpgradeTokenJson(): V2UpgradeTokenJson =
V2UpgradeTokenJson(
wrappedUserKey1 = this.wrappedUserKey1,
wrappedUserKey2 = this.wrappedUserKey2,
)

/**
* Converts the [V2UpgradeTokenJson] into a [V2UpgradeToken].
*/
fun V2UpgradeTokenJson.toV2UpgradeToken(): V2UpgradeToken =
V2UpgradeToken(
wrappedUserKey1 = this.wrappedUserKey1,
wrappedUserKey2 = this.wrappedUserKey2,
)
Loading
Loading