Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal class SdkStateBridge(
@Volatile
private var inMemoryUserKey: SymmetricCryptoKey? = null

@Volatile
private var inMemoryUserKeyId: String? = null

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.

This should not be volatile, but persisted to disk.

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.

@claude Please persist the user key id to disk instead of making it volatile. The key id should be persisted.

@claude claude Bot Aug 18, 2026

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.

Claude finished @quexten's task in 5m 24s —— View job


User key id is now persisted

  • Read SdkStateBridge and the AuthDiskSource persistence patterns
  • Add a persisted userKeyId slot to AuthDiskSource / AuthDiskSourceImpl
  • Wire SdkStateBridge set/get/clear through the disk source
  • Update FakeAuthDiskSource and tests
  • Build + run tests — gradle is not in this run's allowed tools (see note below)
  • Commit and push — 8b27a1a

What changed

AuthDiskSource / AuthDiskSourceImpl — new per-user slot alongside the other cryptographic state:

private const val USER_KEY_ID_KEY = "userKeyId"

override fun getUserKeyId(userId: String): String? =
    getString(key = USER_KEY_ID_KEY.appendIdentifier(userId))

override fun storeUserKeyId(userId: String, userKeyId: String?) {
    putString(key = USER_KEY_ID_KEY.appendIdentifier(userId), value = userKeyId)
}

SdkStateBridge (app/.../sdk/statebridge/SdkStateBridge.kt:42-51) — the @Volatile inMemoryUserKeyId field and the "not persisted" comment are gone; the three callbacks now delegate to the disk source, matching how setEncryptedPin/setV2UpgradeToken/setPersistentPinEnvelope already behave:

override suspend fun setUserKeyId(value: String) {
    authDiskSource.storeUserKeyId(userId = userId, userKeyId = value)
}

override suspend fun getUserKeyId(): String? = authDiskSource.getUserKeyId(userId = userId)

override suspend fun clearUserKeyId() {
    authDiskSource.storeUserKeyId(userId = userId, userKeyId = null)
}

clearData(userId) now clears the key id too (AuthDiskSourceImpl.kt:205), so it is wiped along with the rest of the user's data on logout via UserLogoutManager. The inMemoryUserKey slot is deliberately left volatile — that one is actual key material and must not touch disk.

TestsSdkStateBridgeTest now asserts against the disk source rather than a round-trip through the bridge, FakeAuthDiskSource gained the backing map plus an assertUserKeyId helper and clears the slot in clearData, and AuthDiskSourceTest gained getUserKeyId/storeUserKeyId SharedPreferences tests plus a clearData assertion.

Two things worth your call

  1. Plain vs. encrypted preferences. I used putString/getString (plain bwPreferencesStorage), not putEncryptedString. The key id is an identifier the server already knows, not key material, and this matches how v2UpgradeToken and the persistent PIN envelope are stored. Say the word if you'd rather it live in the encrypted store.
  2. clearUserKeyId now erases the persisted value. That is the literal bridge contract, but note the SDK calls it whenever it tears down the user's crypto state — so if it fires on lock rather than only on logout, the id won't survive a lock/unlock cycle. Worth a look from the SDK side; I didn't add any Android-side special-casing.

Verification

I could not run ./gradlew in this job — every gradle invocation was rejected with "This command requires approval", unlike the earlier run on this PR. So the changes are not compile- or test-verified locally; the change is small and mechanical (interface method + impl + fake + delegation), but CI on this push is the actual check. If you want me to verify locally on a future run, gradle needs to be added to --allowedTools.
· branch sdlc/sdk-update


override suspend fun setUserKey(value: SymmetricCryptoKey) {
inMemoryUserKey = value
}
Expand All @@ -39,6 +42,18 @@ internal class SdkStateBridge(
inMemoryUserKey = null
}

// Nothing on Android consumes the user key id yet, so it is held alongside the in-memory user
// key rather than persisted.
override suspend fun setUserKeyId(value: String) {
inMemoryUserKeyId = value
}

override suspend fun getUserKeyId(): String? = inMemoryUserKeyId

override suspend fun clearUserKeyId() {
inMemoryUserKeyId = null
}

override suspend fun setPersistentPinEnvelope(value: PasswordProtectedKeyEnvelope) {
authDiskSource.storePersistentPinProtectedUserKeyEnvelope(
userId = userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ class SdkStateBridgeTest {
assertNull(stateBridge.getUserKey())
}

@Test
fun `setUserKeyId should store the user key id in memory`() = runTest {
stateBridge.setUserKeyId(value = "userKeyId")

assertEquals("userKeyId", stateBridge.getUserKeyId())
}

@Test
fun `getUserKeyId should return the in-memory user key id`() = runTest {
assertNull(stateBridge.getUserKeyId())

stateBridge.setUserKeyId(value = "userKeyId")

assertEquals("userKeyId", stateBridge.getUserKeyId())
}

@Test
fun `clearUserKeyId should clear the in-memory user key id`() = runTest {
stateBridge.setUserKeyId(value = "userKeyId")

stateBridge.clearUserKeyId()

assertNull(stateBridge.getUserKeyId())
}

@Test
fun `setPersistentPinEnvelope should store the persistent pin envelope`() = runTest {
stateBridge.setPersistentPinEnvelope(value = "pinEnvelope")
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ androidxRoom = "2.8.4"
androidxSecurityCrypto = "1.1.0"
androidxSplash = "1.2.0"
androidxWork = "2.11.2"
bitwardenSdk = "3.0.0-8288-99ffb6ef"
bitwardenSdk = "3.0.0-8379-cc7daf10"
crashlytics = "3.0.7"
detekt = "1.23.8"
firebaseBom = "34.15.0"
Expand Down
Loading