Skip to content

[PM-31056] Migrate PIN unlock after V2 upgrade#1035

Merged
quexten merged 10 commits into
mainfrom
km/pin-migrate-tmp
May 14, 2026
Merged

[PM-31056] Migrate PIN unlock after V2 upgrade#1035
quexten merged 10 commits into
mainfrom
km/pin-migrate-tmp

Conversation

@quexten
Copy link
Copy Markdown
Contributor

@quexten quexten commented May 6, 2026

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-31056

📔 Objective

Implements PIN migration to v2 on unlock. Note: Since not all clients have the state bridge implemented yet, this bails out without error if the state bridge is not implemented.

🚨 Breaking Changes

@quexten quexten force-pushed the km/pin-lock-system branch from 7fa4035 to 83eee71 Compare May 6, 2026 07:17
@quexten quexten force-pushed the km/pin-migrate-tmp branch from 5a335a3 to 526b3df Compare May 6, 2026 07:54
@quexten quexten changed the title Km/pin migrate tmp Implement PIN migration after V2 upgrade May 6, 2026
@quexten quexten changed the title Implement PIN migration after V2 upgrade [PM-31056] Implement PIN migration after V2 upgrade May 6, 2026
@quexten quexten added the ai-review Request a Claude code review label May 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🔍 SDK Breaking Change Detection

SDK Version: km/pin-migrate-tmp (449b5b8)

⚠️ If breaking changes are detected, you MUST have a corresponding pull request addressing the breaking changes ready for merge in the affected client repository.

Client Status Details

| typescript | ✅ No breaking changes detected | Compilation passed with new SDK version - View Details |

| android | ✅ No breaking changes detected | Compilation passed with new SDK version - View Details |

Results update as workflows complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

This PR adds a V1→V2 user-key migration path for the BFU (Before-First-Unlock) persistent PIN envelope, driven from on_unlock. The new migrate_pin_envelope_if_needed enumerates the four (envelope_key_id, current_user_key_id) permutations, uses the stored V2UpgradeToken to recover the V1 key, re-decrypts the stored PIN, and re-enrolls under the current V2 user key. On any migration failure, the caller centrally unenrolls the PIN via unset_pin. The test suite covers the success path, the noop paths, and each MigrationFailed variant.

Code Review Details

No new findings. The active review threads on this PR cover the open discussion points (AFU coverage, error-path centralization) and a follow-up is most useful in those existing threads rather than as new comments.

  • The V1 key unwrapped via V2UpgradeToken::unwrap_v1 is added to the keystore context as a local key inside an immediately-invoked closure; it is dropped when the closure’s ctx falls out of scope, so no V1 key material leaks into the broader session.
  • KeyStoreContext is correctly never held across .await (CLAUDE.md rule satisfied).
  • set_pin clears state before re-sealing; if re-enrollment fails, the caller’s unset_pin is a no-op-equivalent (state is already cleared), so the end result is consistent.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.69%. Comparing base (de0e165) to head (f6f2d7f).
⚠️ Report is 16 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1035      +/-   ##
==========================================
+ Coverage   84.56%   84.69%   +0.12%     
==========================================
  Files         427      430       +3     
  Lines       52930    53459     +529     
==========================================
+ Hits        44762    45277     +515     
- Misses       8168     8182      +14     

☔ View full report in Codecov by Sentry.
📢 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.

Comment on lines +131 to +139
async fn migrate_pin_envelope_if_needed(&self) {
let Some(envelope) = self
.client
.km_state_bridge()
.get_persistent_pin_envelope()
.await
else {
return;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

QUESTION: Is AFU (AfterFirstUnlock) intentionally out of scope for V1→V2 PIN migration?

Details

migrate_pin_envelope_if_needed returns early when no persistent envelope is present, which is the AFU case. However, in AFU mode encrypted_pin is still persisted (so on_unlock can rebuild the ephemeral envelope after each unlock) and that ciphertext is encrypted with the previous user key.

After a V1→V2 user-key upgrade, the AFU flow appears to be:

  1. migrate_pin_envelope_if_needed returns early (no persistent envelope).
  2. on_unlock calls encrypted_pin.decrypt(&mut ctx, SymmetricKeySlotId::User) against the new V2 key.
  3. Decryption fails, the warning "Failed to create PIN envelope" is logged, and no ephemeral envelope is set.
  4. get_pin_lock_type() still returns Some(AfterFirstUnlock) (because encrypted_pin is still present), and get_pin_status() returns NeedsUnlock indefinitely — PIN unlock is silently broken until the user re-enrolls.

If this is intentional (e.g. AFU users are expected to be unaffected because of some flow I'm missing), a code comment in migrate_pin_envelope_if_needed explaining why the early-return branch is safe for AFU would help future readers. If it isn't, AFU users probably want the same V2-upgrade-token-driven recovery path — falling back to unset_pin() so the stale encrypted_pin is cleared and PIN status returns NotSet.

Base automatically changed from km/pin-lock-system to main May 11, 2026 08:17
@quexten quexten requested a review from eligrubb May 11, 2026 08:37
/// PIN envelope is still encrypted with the V1 user key, this function migrates
/// the persistent PIN enrollment to be encrypted with the current user-key.
async fn migrate_pin_envelope_if_needed(&self) {
let Some(envelope) = self
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will be removed when the bridge is merged on all clients.

@quexten quexten marked this pull request as ready for review May 11, 2026 08:38
@quexten quexten requested review from a team as code owners May 11, 2026 08:38
@quexten quexten requested a review from dani-garcia May 11, 2026 08:38
@quexten quexten changed the title [PM-31056] Implement PIN migration after V2 upgrade [PM-31056] Migrate PIN unlock after V2 upgrade May 11, 2026
dani-garcia
dani-garcia previously approved these changes May 12, 2026
warn!("Failed to re-seal PIN envelope after migration, unenrolling PIN");
self.unset_pin().await;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems like every error path is doing self.unset_pin().await; I wonder if it would be easier for the function to return the error and then just do:

if let Err(e) = self.migrate_pin_envelope_if_needed().await {
    self.unset_pin().await;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice. I've made the pin migration return an error enum with the different error states, and have a central log on it (as we do want to have this in e.g. flight recorder), and unenrolling centrally as per your suggestion.

Comment thread crates/bitwarden-core/src/key_management/pin_lock_system.rs Outdated
@quexten
Copy link
Copy Markdown
Contributor Author

quexten commented May 14, 2026

Macos build errors are unrelated and happening on other PRs.

@sonarqubecloud
Copy link
Copy Markdown

@quexten quexten merged commit fe351b4 into main May 14, 2026
59 of 63 checks passed
@quexten quexten deleted the km/pin-migrate-tmp branch May 14, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants