Skip to content

PM-37879: feat: Add SDK state bridge - #7204

Open
david-livefront wants to merge 3 commits into
mainfrom
PM-37879-sdk-state-bridge
Open

PM-37879: feat: Add SDK state bridge#7204
david-livefront wants to merge 3 commits into
mainfrom
PM-37879-sdk-state-bridge

Conversation

@david-livefront

@david-livefront david-livefront commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

🎟️ Tracking

PM-37879

📔 Objective

This PR is a first step in supporting V2 encryption.

  • The V2UpgradeToken is now supported in the SyncResponse and we store that to disk
  • The SdkStateBridge is now in place to support communicating data between the SDK and the main app
  • The V2UpgradeToken is passed into the SDK when unlocking in order to migrate users to V2 encryption

@david-livefront
david-livefront requested a review from quexten July 23, 2026 17:44
@david-livefront
david-livefront requested a review from a team as a code owner July 23, 2026 17:44
@david-livefront david-livefront added the ai-review-vnext Request a Claude code review using the vNext workflow label Jul 23, 2026
@github-actions github-actions Bot added app:password-manager Bitwarden Password Manager app context app:authenticator Bitwarden Authenticator app context t:feature Change Type - Feature Development labels Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Re-reviewed at head 4b40dc9 (prior review was at 100cf78); the PIN envelope separation work has since landed on main via #7218, so this PR now scopes down to three commits: v2UpgradeToken support in the sync response with per-user disk persistence, the user-scoped SdkStateBridge registered against each SDK Client, and passing the stored upgrade token into initializeUserCrypto on unlock. One previously reported finding remains open and unaddressed; no new findings were identified at this head.

Code Review Details

Still open from prior review (existing thread — not re-posted):

  • ⚠️ : setMasterpasswordUnlockData/clearMasterpasswordUnlockData route through updateMasterPasswordUnlock, which unconditionally sets forcePasswordResetReason = null and drives hasMasterPassword off the unlock data. Every other call site is a deliberate password-set/change/key-connector flow; an SDK-driven write is not, so a pending forced password reset can be silently dropped.
    • app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/sdk/statebridge/SdkStateBridge.kt:115

Verified fixed / no longer applicable since the prior review:

  • Encoding a null V2UpgradeTokenJson no longer writes the literal string "null"storeV2UpgradeToken passes null through to putString.
  • All PIN envelope findings (in-memory envelope never cleared, missing flow emission, FakeAuthDiskSource parity) are resolved on main and out of this PR's diff.

Reviewed and found acceptable:

  • V2_UPGRADE_TOKEN stored in the unencrypted SharedPreferences group, consistent with pinKeyEncryptedUserKeyEnvelope, masterKeyEncryptedUserKey, and encOrgKeys; cleared in AuthDiskSourceImpl.clearData and covered by test.
  • UserDecryptionJson.v2UpgradeToken is nullable without a default, which is safe given explicitNulls = false on the shared Json instance in CoreModule.
  • SdkClientManagerImpl registers a fresh bridge per Client; clearCryptodestroyClient drops the client and its in-memory user key on lock and logout.
  • AuthenticatorBridgeRepositoryImpl continues to pass upgradeToken = null intentionally.
  • Sync overwrites the stored token with the server value (including null), which correctly clears it once the server stops issuing one.

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.41096% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.16%. Comparing base (b0daf66) to head (4b40dc9).

Files with missing lines Patch % Lines
...platform/manager/sdk/statebridge/SdkStateBridge.kt 92.00% 0 Missing and 4 partials ⚠️
...rden/data/platform/manager/SdkClientManagerImpl.kt 0.00% 2 Missing ⚠️
...twarden/data/vault/manager/VaultSyncManagerImpl.kt 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7204      +/-   ##
==========================================
- Coverage   86.36%   86.16%   -0.20%     
==========================================
  Files         916      893      -23     
  Lines       65833    64933     -900     
  Branches     9718     9719       +1     
==========================================
- Hits        56854    55948     -906     
- Misses       5509     5511       +2     
- Partials     3470     3474       +4     
Flag Coverage Δ
app-data 17.85% <90.41%> (+0.28%) ⬆️
app-ui-auth-tools 18.71% <0.00%> (-0.05%) ⬇️
app-ui-platform 16.49% <0.00%> (-0.02%) ⬇️
app-ui-vault 27.28% <0.00%> (-0.60%) ⬇️
authenticator 6.09% <0.00%> (-0.02%) ⬇️
lib-core-network-bridge 4.11% <0.00%> (-0.02%) ⬇️
lib-data-ui 1.18% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 4ee8300 to b4ef4d7 Compare July 23, 2026 18:44
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch 2 times, most recently from c28fd0c to a18aa60 Compare July 23, 2026 21:46
}

override suspend fun setEphemeralPinEnvelope(value: PasswordProtectedKeyEnvelope) {
authDiskSource.storePinProtectedUserKeyEnvelope(

@quexten quexten Jul 24, 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.

@david-livefront Both ephemeral and persistent must be present at the same time / that is the expectation from the SDK side. How does this work in auth disk source? It seems the key here is the same? 🤔

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.

That is not how we treat the pin envelope on Android. We always store it in memory and optionally store it to disk based on the inMemoryOnly flag.

    override fun storePinProtectedUserKeyEnvelope(
        userId: String,
        pinProtectedUserKeyEnvelope: String?,
        inMemoryOnly: Boolean,
    ) {
        inMemoryPinProtectedUserKeyEnvelopes[userId] = pinProtectedUserKeyEnvelope
        if (inMemoryOnly) {
            getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
            return
        }
        putString(
            key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId),
            value = pinProtectedUserKeyEnvelope,
        )
        getMutablePinProtectedUserKeyEnvelopeFlow(userId).tryEmit(pinProtectedUserKeyEnvelope)
    }

and then we fetch whatever is available when trying to retrieve the data:

    override fun getPinProtectedUserKeyEnvelope(userId: String): String? =
        inMemoryPinProtectedUserKeyEnvelopes[userId]
            ?: getString(key = PIN_PROTECTED_USER_KEY_KEY_ENVELOPE.appendIdentifier(userId))

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.

In a separate PR, I have fully separated the ephemeral and persistent inProtectedUserKeyEnvelope.

@quexten quexten left a comment

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.

Two notes

@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from a18aa60 to 75e171a Compare July 27, 2026 19:47
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 75e171a to cae440b Compare July 27, 2026 20:21
Comment on lines +119 to +124
override suspend fun setMasterpasswordUnlockData(value: MasterPasswordUnlockData) {
authDiskSource.userState = authDiskSource.userState?.updateMasterPasswordUnlock(
userId = userId,
masterPasswordUnlock = value,
)
}

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.

@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from cae440b to 417d37e Compare July 27, 2026 20:37
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 417d37e to 160b93f Compare July 27, 2026 21:31
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 160b93f to 183c673 Compare July 27, 2026 21:43
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 183c673 to 100cf78 Compare July 28, 2026 19:35
@david-livefront
david-livefront force-pushed the PM-37879-sdk-state-bridge branch from 100cf78 to 4b40dc9 Compare July 29, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review-vnext Request a Claude code review using the vNext workflow app:authenticator Bitwarden Authenticator app context app:password-manager Bitwarden Password Manager app context t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants