Skip to content

Conversation

iinuwa
Copy link

@iinuwa iinuwa commented Oct 6, 2025

🎟️ Tracking

PM-26177

📔 Objective

In order to set up unlock passkeys on mobile clients, this PR adds a method to create a rotateable key set derived from a PRF value.

This is based on existing code in the TypeScript library and web vault:

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
    team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Oct 6, 2025

Logo
Checkmarx One – Scan Summary & Details29191a67-4f6f-4cd0-bfb4-778dbfa424e1

Great job! No new security vulnerabilities introduced in this pull request

Copy link

codecov bot commented Oct 6, 2025

Codecov Report

❌ Patch coverage is 91.71271% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.07%. Comparing base (ae9b8b5) to head (b9bb1f0).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
crates/bitwarden-core/src/key_management/crypto.rs 0.00% 9 Missing ⚠️
...bitwarden-core/src/key_management/crypto_client.rs 0.00% 3 Missing ⚠️
crates/bitwarden-uniffi/src/crypto.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #494      +/-   ##
==========================================
- Coverage   78.10%   78.07%   -0.04%     
==========================================
  Files         283      288       +5     
  Lines       27628    27853     +225     
==========================================
+ Hits        21579    21745     +166     
- Misses       6049     6108      +59     

☔ 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.

@iinuwa iinuwa force-pushed the km/PM-26177/create-prf-user-key-set branch from 2514070 to 32ca764 Compare October 6, 2025 18:30
})
}

fn unlock<Ids: KeyIds>(
Copy link
Author

Choose a reason for hiding this comment

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

This method is currently unused since we don't yet have plans to use the rotateable key set on mobile, only to create them. However, it implements functionality that is currently done in TypeScript that could be moved to the SDK.

I can remove it until we decide to either migrate the browser apps to this SDK method or start using it in the mobile apps.

let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?;
// TODO: There is no method to store only the public key in the store, so we
// have pull out the encryption key to encapsulate it manually.
Copy link
Contributor

Choose a reason for hiding this comment

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

Note: I think this needs a KM-owned tech debt follow-up ticket. I'll make a jira ticket when this merges, but adding the APIs for this feels out of scope for an external PR.

Copy link
Contributor

@quexten quexten left a comment

Choose a reason for hiding this comment

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

Changes look mostly good, thank you!
Only some minor questions / nits.

One more (optional) suggestion here would be to take test vectors from the TS clients and feed them into the unit tests so we know the implementation works the same way.

@iinuwa
Copy link
Author

iinuwa commented Oct 7, 2025

Thank you for the review!

One more (optional) suggestion here would be to take test vectors from the TS clients and feed them into the unit tests so we know the implementation works the same way.

I looked around the clients codebase, but I don't see a full test vector for an end-to-end (PRF, User Key) -> RotateableKeySet and back. There are mocked values that don't verify the actual cryptography in https://github.com/bitwarden/clients/blob/main/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts and https://github.com/bitwarden/clients/blob/main/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts. The Rust types guarantee some of the other invariants tested in. Did you have some specific test vectors in mind?

@abergs
Copy link
Member

abergs commented Oct 7, 2025

@iinuwa just a ping that the lint failed and needs a cargo +"nightly-2025-08-18" fmt

@iinuwa
Copy link
Author

iinuwa commented Oct 7, 2025

@iinuwa just a ping that the lint failed and needs a cargo +"nightly-2025-08-18" fmt

Thanks. I'm using jj which doesn't run git hooks, and my IDE formats according to Rust stable formatting rules, but doesn't format comments the same way. I'll work that out in my workflow.

Copy link
Contributor

@quexten quexten Oct 8, 2025

Choose a reason for hiding this comment

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

Note: I'd recommend moving RotateableKeySet into it's own file, since it is a different cryptographic construction. key_rotation.rs is really just meant for "User Key Rotation", which may interact with RotateableKeySets, but is a different use-case.

(As a KM note, key_rotation.rs should probably be renamed to user_key_rotation.rs)

Copy link

claude bot commented Oct 8, 2025

Claude finished @iinuwa's task —— View job


PR Review: Add methods to create rotateable key sets from PRF

Review Tasks:

  • Read repository guidelines (CLAUDE.md) - No repo-specific guidelines found
  • Analyze main implementation files
  • Review cryptographic correctness and security
  • Check code quality and best practices
  • Examine test coverage and testing approach
  • Provide comprehensive feedback

Summary

This PR successfully implements PRF-based rotateable key set creation for mobile unlock passkeys. The implementation follows established patterns in the codebase and addresses the core requirements. However, there are several areas that need attention.

🎨 Code Quality & Architecture

Positive Aspects

  • Consistent patterns: The implementation follows existing cryptographic patterns in the codebase (e.g., similar to master_key.rs and pin_key.rs)
  • Good separation of concerns: PRF logic is cleanly separated in its own module
  • Proper error handling: Uses appropriate CryptoError types throughout
  • Clear documentation: Good docstrings explaining the purpose and requirements

Areas for Improvement

crates/bitwarden-crypto/src/keys/prf.rs

🎨 Minor Issues:

  1. Line 49-50: The test test_long_prf_truncated_to_proper_length has a logic error:

    let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap();
    let key2 = derive_symmetric_key_from_prf(&prf).unwrap();
    assert_eq!(key1, key2);  // This will likely fail!

    The test creates two different pseudorandom byte sequences but expects them to be equal. It should use the same first 32 bytes:

    let long_prf = pseudorandom_bytes(64);
    let truncated_prf = &long_prf[..32];
    let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap();
    let key2 = derive_symmetric_key_from_prf(truncated_prf).unwrap();
  2. Line 10: The zero-check validation is good but consider documenting why all-zeros PRF is dangerous

  3. Line 13-15: The Box::pin() conversion seems unnecessary - consider if this can be simplified

crates/bitwarden-crypto/src/store/key_rotation.rs

📝 Observations:

  1. Architecture suggestion: As noted in review comments, consider moving RotateableKeySet to its own file since it's a distinct cryptographic primitive
  2. Line 91: Good use of #[allow(deprecated)] with clear reasoning in comments
  3. Lines 119-137: The unlock method is currently dead code but well-implemented for future migration

crates/bitwarden-core/src/key_management/crypto.rs

✅ Implementation looks correct:

  • Proper error propagation
  • Correct use of existing key store context
  • Good integration with existing patterns

🔒 Security Analysis

Cryptographic Correctness

  • PRF truncation: Correctly truncates PRF to 32 bytes, which is appropriate for the security level
  • Key derivation: Uses existing stretch_key function via HKDF, maintaining consistency
  • Zero validation: Prevents use of uninitialized/zero PRF values
  • Key encapsulation: Follows established patterns for public key encryption of downstream keys

Security Concerns

  • ⚠️ PRF source validation: The implementation trusts that the PRF input is cryptographically secure. Consider adding documentation about PRF requirements
  • No sensitive data leakage: Proper handling of key material throughout

🧪 Testing

Test Coverage

  • Basic functionality: Well covered with positive and negative cases
  • Error cases: Tests for short PRF and zero PRF rejection
  • Test bug: As noted above, test_long_prf_truncated_to_proper_length has incorrect logic
  • 📝 Missing coverage: No end-to-end integration tests (though this may be intentional for this PR scope)

Test Quality

The tests in prf.rs are well-structured but the truncation test needs fixing. The rotateable key set tests in key_rotation.rs are comprehensive and demonstrate proper usage patterns.

🚀 Performance & Efficiency

  • Minimal overhead: Adds minimal computational overhead
  • Memory efficient: No unnecessary allocations or copies
  • Lazy evaluation: Keys are generated on-demand

📋 API Design

  • Consistent naming: Follows established conventions (make_prf_user_key_set)
  • Proper error types: Returns appropriate CryptoClientError/CryptoError
  • Type safety: Good use of type system to prevent misuse

⚠️ Issues Requiring Attention

  1. Critical: Fix the test logic bug in test_long_prf_truncated_to_proper_length
  2. Minor: Consider documentation improvements for PRF security requirements
  3. Future: Address the architectural suggestion to move RotateableKeySet to its own file

🔧 Recommendations

  1. Fix the test: Update the truncation test to properly validate that long PRFs are correctly truncated
  2. Add PRF documentation: Document expected PRF entropy and security requirements
  3. Consider test vectors: If available from TypeScript implementation, add cross-platform test vectors

✅ Overall Assessment

This is a well-implemented PR that successfully adds the requested functionality while maintaining code quality and security standards. The cryptographic approach is sound and follows established patterns. With the test fix, this would be ready for merge.

The implementation correctly addresses the objective of enabling unlock passkeys on mobile clients through PRF-derived rotateable key sets.


Copy link

sonarqubecloud bot commented Oct 8, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants