Skip to content

Conversation

@quexten
Copy link
Contributor

@quexten quexten commented Oct 20, 2025

🎟️ Tracking

📔 Objective

Forces cipher keys when upgrading to crypto v2.

⏰ 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

@claude
Copy link

claude bot commented Oct 20, 2025

Claude finished @quexten's task —— View job


Code Review Complete

Summary of Changes

This PR introduces mandatory cipher key encryption for users on crypto v2 by adding a security version check alongside the existing feature flag. The changes ensure that all ciphers for v2 users will have cipher keys generated during encryption operations, even if the feature flag is disabled.

Files Changed:

  • crates/bitwarden-core/src/key_management/security_state.rs - Added new constant
  • crates/bitwarden-core/src/key_management/mod.rs - Exported new constant
  • crates/bitwarden-vault/src/cipher/cipher_client.rs - Updated cipher key generation logic

Critical Issues

❌ Logic Error: Incorrect Boolean Condition

Location: cipher_client.rs:36-43 and cipher_client.rs:87-94

The current implementation uses an OR condition (||) which creates unexpected behavior:

if cipher_view.key.is_none()
    && (self.client.internal.get_flags().enable_cipher_key_encryption
        || self.client.internal.get_security_version() >= MINIMUM_ENFORCE_CIPHER_KEY_ENCRYPTION_VERSION)

Problem: This means cipher keys will be generated if EITHER:

  • The feature flag is enabled, OR
  • The security version is >= 2

However, the security version check should only apply to v2 users (those with security_version >= 2). The issue is that v1 users have security_version = 1 (per internal.rs:261-267), so for v1 users:

  • If flag is ON: cipher keys are generated ✓ (correct)
  • If flag is OFF: cipher keys are NOT generated ✓ (correct)

For v2 users (who have security_version = 2):

  • If flag is ON: cipher keys are generated ✓ (correct)
  • If flag is OFF: cipher keys are STILL generated ✓ (correct per PR intent)

Actually, upon careful analysis, the logic appears correct! The OR condition properly handles both cases:

  • V1 users only generate cipher keys when the flag is enabled
  • V2 users always generate cipher keys (since their version >= 2)

However, there's a subtle issue: The constant name and comment suggest this is about "enforcing presence" but the code is about "forcing generation". These are different concepts.

Detailed Logic Analysis

Looking at internal.rs:261-267:

pub fn get_security_version(&self) -> u64 {
    self.security_state
        .read()
        .expect("RwLock is not poisoned")
        .as_ref()
        .map_or(1, |state| state.version())
}

V1 users: security_state = None → returns 1
V2 users: security_state = Some(SecurityState { version: 2, .. }) → returns 2

So the condition works correctly:

  • V1 users (version=1): false || (1 >= 2) = false || false = false → Only generates if flag is true
  • V2 users (version=2): false || (2 >= 2) = false || true = true → Always generates

🤔 Semantic Concern: Constant Naming vs. Actual Behavior

Location: security_state.rs:37

pub const MINIMUM_ENFORCE_CIPHER_KEY_ENCRYPTION_VERSION: u64 = 2;

The constant is named MINIMUM_ENFORCE_CIPHER_KEY_ENCRYPTION but the code uses it to force generation of cipher keys, not to enforce their presence on existing ciphers.

Questions:

  1. Should this PR also add validation to reject/upgrade existing ciphers without keys?
  2. Is the intent only to force generation going forward, or also to validate existing data?
  3. Should there be a separate validation pass for existing ciphers in user vaults?

The comment says "enforces their presence" but the implementation only ensures generation during encryption operations. Existing ciphers without keys won't be touched unless they're edited.


Suggested Improvements

📝 Missing Documentation

The PR objective states "Forces cipher keys when upgrading to crypto v2" but there's no documentation about:

  1. Migration path: What happens to existing v2 ciphers without keys? They'll only get keys when next edited.
  2. Rollout strategy: Is this safe to deploy immediately, or does it require coordination?
  3. Backward compatibility: Can v2 users still read old ciphers without keys? (Yes, per the code, but should be documented)

🎨 Consider Adding Validation

Potential enhancement for future PR

Consider adding a validation method that checks if v2 users have any ciphers without keys:

pub fn validate_cipher_keys_for_v2(&self, ciphers: &[Cipher]) -> Result<(), ValidationError> {
    if self.client.internal.get_security_version() >= MINIMUM_ENFORCE_CIPHER_KEY_ENCRYPTION_VERSION {
        for cipher in ciphers {
            if cipher.key.is_none() {
                return Err(ValidationError::MissingCipherKey(cipher.id));
            }
        }
    }
    Ok(())
}

This would provide defense-in-depth against any server-side downgrade attempts.

🎨 Code Clarity

Location: cipher_client.rs:36-43

Consider extracting the condition into a well-named helper method:

fn should_generate_cipher_key(&self, cipher_has_key: bool) -> bool {
    !cipher_has_key && (
        self.client.internal.get_flags().enable_cipher_key_encryption
        || self.client.internal.get_security_version() >= MINIMUM_ENFORCE_CIPHER_KEY_ENCRYPTION_VERSION
    )
}

This would:

  • Make the duplicated logic (appears twice) DRY
  • Improve readability
  • Make the intent clearer
  • Simplify future updates

Security Analysis

✅ Security Implications

Per CLAUDE.md requirements for encryption/decryption modifications:

Backward Compatibility: ✓ VERIFIED

  • Existing encrypted data remains decryptable
  • V1 users are unaffected
  • V2 users can still decrypt old ciphers without keys (the generate_cipher_key method handles re-encrypting attachments/FIDO2 credentials with the new key)

Downgrade Protection: ✓ GOOD

  • Server cannot downgrade v2 users since security state is signed by the user
  • Server cannot force ciphers to not have keys for v2 users going forward
  • However, existing v2 ciphers without keys are not rejected (by design, for gradual migration)

No Secrets Leaked: ✓ VERIFIED

  • No logging of sensitive data
  • No exposure of keys in error paths

Good Practices Observed

  • ✓ Minimal, focused change
  • ✓ Consistent with existing MINIMUM_ENFORCE_ICON_URI_HASH_VERSION pattern
  • ✓ Proper constant export through module hierarchy
  • ✓ Existing tests continue to pass (per CI)

Action Items

For Author:

  1. Clarify intent: Is this PR only about forcing generation going forward, or should there also be validation of existing ciphers? The constant name suggests enforcement, but implementation is generation-only.

  2. 📝 Add comment/documentation explaining the migration strategy:

    • How existing v2 ciphers without keys are handled (gradual upgrade on edit)
    • Why immediate validation isn't enforced (if that's intentional)
    • Timeline for when all v2 ciphers should have keys
  3. 🎨 Consider refactoring the duplicated condition into a helper method for better maintainability

  4. ⚠️ Verify test coverage: Are there tests that specifically cover:

    • V2 user with flag OFF generates cipher keys?
    • V1 user with flag OFF does NOT generate cipher keys?
    • V2 user can still decrypt old ciphers without keys?

Optional Enhancement:

  • Consider adding validation logic in a follow-up PR if the goal is to enforce presence of cipher keys on v2 users

@github-actions
Copy link
Contributor

github-actions bot commented Oct 20, 2025

Logo
Checkmarx One – Scan Summary & Detailsa5ffd256-020f-4524-97a5-a1a541120abf

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

@sonarqubecloud
Copy link

@codecov
Copy link

codecov bot commented Oct 20, 2025

Codecov Report

❌ Patch coverage is 33.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.34%. Comparing base (15e947a) to head (4ecd3eb).

Files with missing lines Patch % Lines
crates/bitwarden-vault/src/cipher/cipher_client.rs 33.33% 4 Missing ⚠️
Additional details and impacted files
@@                        Coverage Diff                         @@
##           km/icon-url-checksum-crypto-v2     #520      +/-   ##
==================================================================
- Coverage                           78.35%   78.34%   -0.02%     
==================================================================
  Files                                 287      287              
  Lines                               28118    28122       +4     
==================================================================
  Hits                                22033    22033              
- Misses                               6085     6089       +4     

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

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.

2 participants