Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a10fef6
[PM-24468] Introduce CipherRiskClient
shane-melton Oct 9, 2025
12f17c0
[PM-24468] Add concurrency support for compute_risk in CipherRiskClient
shane-melton Oct 13, 2025
cffd1df
[PM-24468] Use Arc for password_map to avoid expensive clones
shane-melton Oct 13, 2025
d803671
[PM-24468] Fix magic values
shane-melton Oct 13, 2025
b327c24
[PM-24468] Remove redundant tests
shane-melton Oct 14, 2025
cc28aab
[PM-24468] Sort Vault Cargo.toml
shane-melton Oct 14, 2025
25ecee3
[PM-24468] Fix cargo.toml formatting
shane-melton Oct 14, 2025
5daddda
[PM-24468] Better empty password handling
shane-melton Oct 14, 2025
56fe325
[PM-24468] Strip URL from HIBP errors
shane-melton Oct 14, 2025
c5b7e40
[PM-24468] Use Option<u32> for reuse_count result
shane-melton Oct 14, 2025
1956e61
[PM-24468] Refactor HIBP exposure handling to capture errors per-ciphโ€ฆ
shane-melton Oct 14, 2025
678856f
[PM-24468] Update exposed password result type
shane-melton Oct 14, 2025
6fe90ea
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 14, 2025
3a97ee0
[PM-24468] Add default serialization for CipherRiskOptions fields
shane-melton Oct 15, 2025
1ea9850
[PM-24468] Use option chaining and iterators to improve readability
shane-melton Oct 15, 2025
5cc8069
[PM-24468] Use constructor function for PasswordReuseMap
shane-melton Oct 15, 2025
757fda1
[PM-24468] Expose ExposedPasswordResult enum
shane-melton Oct 15, 2025
b7d3077
[PM-24468] Add to_cipher_risk helper
shane-melton Oct 15, 2025
76f83f7
[PM-24468] Remove unnecessary Result return type
shane-melton Oct 15, 2025
4c1cce2
[PM-24468] Revert making password_reuse_map static
shane-melton Oct 15, 2025
02e0ce5
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 16, 2025
ed87d35
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 21, 2025
d201755
[PM-24468] Remove unused to_login_details helper
shane-melton Oct 21, 2025
9b8b001
[PM-24468] Make CipherId required for CipherLoginDetails and CipherRisk
shane-melton Oct 21, 2025
5d0e1d2
[PM-24468] Move cipher_risk to its own module
shane-melton Oct 21, 2025
a30d67b
[PM-24468] Move HIBP logic to its own module
shane-melton Oct 21, 2025
ac5c2f0
[PM-24468] Cleanup cipher risk module exports
shane-melton Oct 21, 2025
4795415
[PM-24468] Rename CipherRisk to CipherRiskResult
shane-melton Oct 21, 2025
c039308
[PM-24468] Update futures and zxcvbn dependencies to use workspace coโ€ฆ
shane-melton Oct 21, 2025
c90e779
[PM-24468] Formatting
shane-melton Oct 21, 2025
46a1b7a
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 23, 2025
bc1808f
[PM-24468] Remove unused pub(super)
shane-melton Oct 23, 2025
5f76d84
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 23, 2025
02f5614
[PM-24468] Use workspace version of zxcvbn in bitwarden-core
shane-melton Oct 27, 2025
8f99253
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 27, 2025
a192f62
Merge branch 'main' into vault/pm-24468/cipher-risk-client
shane-melton Oct 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/bitwarden-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ bitwarden-state = { workspace = true }
bitwarden-uuid = { workspace = true }
chrono = { workspace = true }
data-encoding = { workspace = true }
futures = "0.3"
hmac = ">=0.12.1, <0.13"
percent-encoding = ">=2.1, <3.0"
reqwest = { workspace = true }
Expand All @@ -55,11 +56,13 @@ uniffi = { workspace = true, optional = true }
uuid = { workspace = true }
wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
zxcvbn = ">=3.0.1, <4.0"

[dev-dependencies]
bitwarden-api-api = { workspace = true, features = ["mockall"] }
bitwarden-test = { workspace = true }
tokio = { workspace = true, features = ["rt"] }
wiremock = { workspace = true }

[lints]
workspace = true
17 changes: 17 additions & 0 deletions crates/bitwarden-vault/src/cipher/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,23 @@ impl CipherView {
}
}

/// Extract login details for risk evaluation (login ciphers only).
///
/// Returns `Some(CipherLoginDetails)` if this is a login cipher with a password,
/// otherwise returns `None`.
pub fn to_login_details(&self) -> Option<crate::cipher::cipher_risk::CipherLoginDetails> {
if let Some(login) = &self.login {
if let Some(password) = &login.password {
return Some(crate::cipher::cipher_risk::CipherLoginDetails {
id: self.id,
password: password.clone(),
username: login.username.clone(),
});
}
}
None
}

fn reencrypt_attachment_keys(
&mut self,
ctx: &mut KeyStoreContext<KeyIds>,
Expand Down
91 changes: 91 additions & 0 deletions crates/bitwarden-vault/src/cipher/cipher_risk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use {tsify::Tsify, wasm_bindgen::prelude::*};

use crate::CipherId;

/// Result of checking password exposure via HIBP API.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[serde(tag = "type", content = "value")]
pub enum ExposedPasswordResult {
/// Password exposure check was not performed (check_exposed was false or password was empty)
NotChecked,
/// Successfully checked, found in this many breaches
Found(u32),
/// HIBP API request failed with error message
Error(String),
}

/// Login cipher data needed for risk evaluation.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct CipherLoginDetails {
/// Cipher ID to identify which cipher in results.
pub id: Option<CipherId>,
/// The decrypted password to evaluate.
pub password: String,
/// Username or email (login ciphers only have one field).
pub username: Option<String>,
}

/// Password reuse map wrapper for WASM compatibility.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[serde(transparent)]
pub struct PasswordReuseMap {
/// Map of passwords to their occurrence count.
#[cfg_attr(feature = "wasm", tsify(type = "Record<string, number>"))]
pub map: HashMap<String, u32>,
}

/// Options for configuring risk computation.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
pub struct CipherRiskOptions {
/// Pre-computed password reuse map (password โ†’ count).
/// If provided, enables reuse detection across ciphers.
pub password_map: Option<PasswordReuseMap>,
/// Whether to check passwords against Have I Been Pwned API.
/// When true, makes network requests to check for exposed passwords.
pub check_exposed: bool,
/// Optional HIBP API base URL override. When None, uses the production HIBP URL.
/// Can be used for testing or alternative password breach checking services.
pub hibp_base_url: Option<String>,
}

/// Risk evaluation result for a single cipher.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct CipherRisk {
/// Cipher ID matching the input CipherLoginDetails.
pub id: Option<CipherId>,
/// Password strength score from 0 (weakest) to 4 (strongest).
/// Calculated using zxcvbn with cipher-specific context.
pub password_strength: u8,
/// Result of checking password exposure via HIBP API.
/// - `NotChecked`: check_exposed was false, or password was empty
/// - `Found(n)`: Successfully checked, found in n breaches
/// - `Error(msg)`: HIBP API request failed for this cipher with the given error message
pub exposed_result: ExposedPasswordResult,
/// Number of times this password appears in the provided password_map.
/// None if not found or if no password_map was provided.
pub reuse_count: Option<u32>,
}

#[cfg(feature = "wasm")]
impl wasm_bindgen::__rt::VectorIntoJsValue for CipherRisk {
fn vector_into_jsvalue(
vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>,
) -> wasm_bindgen::JsValue {
wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector)
}
}
Loading
Loading