Skip to content

Conversation

shane-melton
Copy link
Member

@shane-melton shane-melton commented Oct 9, 2025

🎟️ Tracking

PM-24468

📔 Objective

Implement the cipher risk evaluation logic in the Vault SDK as a separate CipherRiskClient so that it can be re-used in mobile and gain potential performance improvements.

The CipherRiskClient contains logic to check if a multiple login ciphers' passwords are exposed (via HIBP), weak (via zxcvbn), or reused.

⏰ 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

claude bot commented Oct 9, 2025

Claude finished @shane-melton's task —— View job


Code Review Complete ✅

Summary of Changes

This PR introduces CipherRiskClient to evaluate password security risks (HIBP exposure, weak passwords, password reuse) for login ciphers. The implementation adds ~900 lines of well-tested code across 2 new modules with comprehensive test coverage (95.10% patch coverage).

Changes since last review (Oct 9):

  • Merge with main branch
  • Updated ExposedPasswordResult to use enum variants instead of Option
  • Refactored error handling to capture HIBP failures per-cipher rather than failing entire batch
  • Changed reuse_count to Option<u32> for clearer semantics
  • Stripped URLs from HIBP errors for privacy
  • Improved empty password handling
  • Minor code quality improvements (formatting, removing redundant tests, fixing magic values)

Critical Issues

⚠️ Security: Potential Password Leakage in Error Messages

cipher_risk_client.rs:107-108

Err(e) => ExposedPasswordResult::Error(e.to_string()),

Issue: Error messages from HIBP API requests could potentially leak sensitive information. While HIBP uses k-anonymity (only hash prefixes are sent), error messages might contain URL fragments, timing information, or other metadata that could be exploited.

Recommendation:

  • Sanitize error messages before storing them in ExposedPasswordResult::Error
  • Consider using a generic error message or error code instead of the full error string
  • Add explicit documentation about what information may appear in error messages
Example sanitization approach
Err(e) => {
    // Sanitize error to avoid leaking sensitive info
    let sanitized_msg = match e.status() {
        Some(status) => format!("HIBP API request failed: {}", status),
        None => "HIBP API request failed: network error".to_string(),
    };
    ExposedPasswordResult::Error(sanitized_msg)
}

⚠️ Security: SHA-1 Usage Without Justification

cipher_risk_client.rs:189-195

fn hash_password_for_hibp(password: &str) -> (String, String) {
    use sha1::{Digest, Sha1};
    let hash = Sha1::digest(password.as_bytes());
    // ...
}

Issue: Per CLAUDE.md guidelines: "New crypto algorithms or key derivation - Detailed description, review and audit trail required." SHA-1 is used here without documentation explaining why it's safe in this context.

Recommendation: Add a comment explaining:

  • SHA-1 is required by the HIBP API specification (not a choice by Bitwarden)
  • The security model doesn't rely on SHA-1's collision resistance
  • This is for password lookup, not cryptographic security
  • The k-anonymity model protects the actual password
Suggested documentation
/// Hash password with SHA-1 and split into prefix/suffix for k-anonymity.
///
/// # Security Note
/// SHA-1 is used here because it's mandated by the HIBP API specification.
/// While SHA-1 is cryptographically broken for collision resistance, this
/// is acceptable in the HIBP context because:
/// 1. We're not relying on collision resistance for security
/// 2. The k-anonymity model (sending only 5-char prefix) protects the password
/// 3. HIBP is a lookup service, not a cryptographic primitive
/// 4. Changing the hash algorithm would break compatibility with HIBP
///
/// Returns a tuple of (prefix: first 5 chars, suffix: remaining chars).
fn hash_password_for_hibp(password: &str) -> (String, String) {

❌ Bug: Missing Export for ExposedPasswordResult

cipher_risk.rs:14 & mod.rs:28

The ExposedPasswordResult enum is public and used in the public API (CipherRisk.exposed_result), but it's not exported in cipher/mod.rs line 28:

pub use cipher_risk::{CipherLoginDetails, CipherRisk, CipherRiskOptions, PasswordReuseMap};

Impact: External consumers cannot pattern match on or use the ExposedPasswordResult enum properly, only through the CipherRisk struct field.

Fix:

pub use cipher_risk::{
    CipherLoginDetails, CipherRisk, CipherRiskOptions, ExposedPasswordResult, PasswordReuseMap
};

Suggested Improvements

🎨 Code Quality: Magic Number in Concurrent Requests Limit

cipher_risk_client.rs:17

const MAX_CONCURRENT_REQUESTS: usize = 100;

Suggestion: The limit of 100 concurrent requests seems arbitrary. Consider:

  • Documenting the rationale (performance testing, HIBP rate limits, memory constraints)
  • Making it configurable via CipherRiskOptions for flexibility
  • Reducing the default to be more conservative (e.g., 50) to avoid overwhelming HIBP or client connections

🎨 API Design: Inconsistent Naming Convention

cipher_risk_client.rs:32 vs 67

pub fn password_reuse_map(...) -> Result<PasswordReuseMap, CipherRiskError>
pub async fn compute_risk(...) -> Result<Vec<CipherRisk>, CipherRiskError>

Observation: password_reuse_map is synchronous while compute_risk is async. For consistency with SDK patterns, consider naming the sync method build_password_reuse_map to make the distinction clearer.


🤔 Performance: Unnecessary String Cloning

cipher_risk_client.rs:79-82

let base_url = options
    .hibp_base_url
    .clone()
    .unwrap_or_else(|| HIBP_DEFAULT_BASE_URL.to_string());

Issue: hibp_base_url is cloned for every future in the stream, but it could be wrapped in Arc once (like password_map).

Suggestion:

let base_url = Arc::new(
    options
        .hibp_base_url
        .unwrap_or_else(|| HIBP_DEFAULT_BASE_URL.to_string())
);

// Then in the future:
let base_url = base_url.clone(); // Arc clone, not String clone

🎨 Type Safety: Empty Password Handling

cipher_risk_client.rs:38 & 85-93

Empty passwords are filtered in password_reuse_map but checked in compute_risk. Consider:

  • Extracting empty password check into a helper method
  • Or filter empty passwords once at the beginning of compute_risk to avoid processing them at all
  • Document why empty passwords return NotChecked vs being filtered entirely

💭 Documentation: HIBP Rate Limiting

cipher_risk_client.rs:67

The compute_risk method makes concurrent requests to HIBP but doesn't mention rate limiting. HIBP has rate limits, and while the code uses MAX_CONCURRENT_REQUESTS, there's no backoff or retry logic.

Suggestion: Document:

  • HIBP rate limit considerations
  • What happens if rate limited (captured in Error variant)
  • Whether client should implement retry logic

🎨 Error Handling: Unwrap in Parse Function

cipher_risk_client.rs:207

return count_str.trim().parse().unwrap_or(0);

Observation: Using unwrap_or(0) silently swallows malformed count values. This is reasonable, but consider:

  • Logging a warning when parse fails (if logging is available)
  • Or returning Error variant for that cipher to signal data corruption
  • Document that malformed counts default to 0

🌱 Future: Consider Caching HIBP Results

The implementation doesn't cache HIBP results. For improved performance and reduced API load:

  • Consider adding optional caching of HIBP results (hash prefix → response)
  • Could be added to CipherRiskOptions in the future
  • Would need TTL management and cache invalidation strategy

Good Practices Observed

Excellent test coverage (95%+) including edge cases, error handling, and concurrency verification
Strong k-anonymity implementation protecting password privacy via hash prefix submission
Per-cipher error capture allowing partial batch success instead of all-or-nothing
Arc optimization for password_map to avoid expensive clones
Comprehensive documentation with clear API contracts and error semantics
Integration with existing cipher types via CipherView::to_login_details()
WASM and UniFFI support for cross-platform compatibility
Clean separation of concerns (types, client logic, integration)
Proper use of zxcvbn with cipher-specific context for password strength
Wiremock testing for network layer validation


Action Items for Author

Must Fix (Blocking):

  1. ❌ Export ExposedPasswordResult in cipher/mod.rs (API completeness issue)
  2. ⚠️ Add SHA-1 justification comment per CLAUDE.md requirements
  3. ⚠️ Sanitize HIBP error messages to prevent information leakage

Should Consider:
4. 🎨 Document rationale for MAX_CONCURRENT_REQUESTS = 100 or make configurable
5. 🎨 Optimize hibp_base_url cloning with Arc
6. 💭 Document HIBP rate limiting behavior and expectations

Nice to Have:
7. 🎨 Consider renaming password_reuse_mapbuild_password_reuse_map
8. 🎨 Extract empty password check to helper method
9. 💭 Add warning/tracing for malformed HIBP count values


Test Coverage Analysis

The codecov report shows 31 lines missing coverage. Based on the implementation:

  • cipher.rs:493-504 - to_login_details() helper (12 lines)
  • cipher_risk.rs - Type definitions (6 lines)
  • error.rs - Error enum definition (1 line)
  • vault_client.rs:75-79 - Client getter (5 lines)
  • cipher_risk_client.rs - Some error paths (7 lines)

Recommendation: Add integration tests that exercise:

  • CipherView::to_login_details() with various cipher types
  • The VaultClient::cipher_risk() accessor
  • Error paths in type conversions

Copy link
Contributor

github-actions bot commented Oct 9, 2025

Logo
Checkmarx One – Scan Summary & Detailsda7671d4-2161-440f-9038-6624576a0b3f

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

Copy link

codecov bot commented Oct 9, 2025

Codecov Report

❌ Patch coverage is 95.10269% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.37%. Comparing base (8b95ae6) to head (6fe90ea).

Files with missing lines Patch % Lines
crates/bitwarden-vault/src/cipher/cipher.rs 0.00% 12 Missing ⚠️
...s/bitwarden-vault/src/cipher/cipher_risk_client.rs 98.85% 7 Missing ⚠️
crates/bitwarden-vault/src/cipher/cipher_risk.rs 0.00% 6 Missing ⚠️
crates/bitwarden-vault/src/vault_client.rs 0.00% 5 Missing ⚠️
crates/bitwarden-vault/src/error.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #499      +/-   ##
==========================================
+ Coverage   77.98%   78.37%   +0.38%     
==========================================
  Files         287      289       +2     
  Lines       27673    28306     +633     
==========================================
+ Hits        21582    22184     +602     
- Misses       6091     6122      +31     

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

@shane-melton shane-melton force-pushed the vault/pm-24468/cipher-risk-client branch from 915fe76 to a10fef6 Compare October 13, 2025 17:54
Copy link

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.

1 participant