Skip to content

Commit a10fef6

Browse files
committed
[PM-24468] Introduce CipherRiskClient
1 parent 80fbec9 commit a10fef6

File tree

9 files changed

+765
-4
lines changed

9 files changed

+765
-4
lines changed

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-vault/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ uniffi = { workspace = true, optional = true }
5555
uuid = { workspace = true }
5656
wasm-bindgen = { workspace = true, optional = true }
5757
wasm-bindgen-futures = { workspace = true, optional = true }
58+
zxcvbn = ">=3.0.1, <4.0"
5859

5960
[dev-dependencies]
6061
bitwarden-api-api = { workspace = true, features = ["mockall"] }
6162
bitwarden-test = { workspace = true }
6263
tokio = { workspace = true, features = ["rt"] }
64+
wiremock = { workspace = true }
6365

6466
[lints]
6567
workspace = true

crates/bitwarden-vault/src/cipher/cipher.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,23 @@ impl CipherView {
486486
}
487487
}
488488

489+
/// Extract login details for risk evaluation (login ciphers only).
490+
///
491+
/// Returns `Some(CipherLoginDetails)` if this is a login cipher with a password,
492+
/// otherwise returns `None`.
493+
pub fn to_login_details(&self) -> Option<crate::cipher::cipher_risk::CipherLoginDetails> {
494+
if let Some(login) = &self.login {
495+
if let Some(password) = &login.password {
496+
return Some(crate::cipher::cipher_risk::CipherLoginDetails {
497+
id: self.id,
498+
password: password.clone(),
499+
username: login.username.clone(),
500+
});
501+
}
502+
}
503+
None
504+
}
505+
489506
fn reencrypt_attachment_keys(
490507
&mut self,
491508
ctx: &mut KeyStoreContext<KeyIds>,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use std::collections::HashMap;
2+
3+
use serde::{Deserialize, Serialize};
4+
#[cfg(feature = "wasm")]
5+
use {tsify::Tsify, wasm_bindgen::prelude::*};
6+
7+
use crate::CipherId;
8+
9+
/// Minimal login cipher data needed for risk evaluation
10+
#[derive(Serialize, Deserialize, Debug, Clone)]
11+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
12+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
13+
pub struct CipherLoginDetails {
14+
/// Cipher ID to identify which cipher in results
15+
pub id: Option<CipherId>,
16+
/// The decrypted password to evaluate
17+
pub password: String,
18+
/// Username or email (login ciphers only have one field)
19+
pub username: Option<String>,
20+
}
21+
22+
/// Password reuse map wrapper for WASM compatibility
23+
#[derive(Serialize, Deserialize, Debug, Clone)]
24+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
25+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
26+
#[serde(transparent)]
27+
pub struct PasswordReuseMap {
28+
/// Map of passwords to their occurrence count
29+
#[cfg_attr(feature = "wasm", tsify(type = "Record<string, number>"))]
30+
pub map: HashMap<String, u32>,
31+
}
32+
33+
/// Options for configuring risk computation
34+
#[derive(Serialize, Deserialize, Debug, Clone)]
35+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
36+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
37+
#[serde(rename_all = "camelCase")]
38+
#[derive(Default)]
39+
pub struct CipherRiskOptions {
40+
/// Pre-computed password reuse map (password → count)
41+
/// If provided, enables reuse detection across ciphers
42+
pub password_map: Option<PasswordReuseMap>,
43+
/// Whether to check passwords against Have I Been Pwned API
44+
/// When true, makes network requests to check for exposed passwords
45+
pub check_exposed: bool,
46+
}
47+
48+
/// Risk evaluation result for a single cipher
49+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
50+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
51+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
52+
pub struct CipherRisk {
53+
/// Cipher ID matching the input CipherLoginDetails
54+
pub id: Option<CipherId>,
55+
/// Password strength score from 0 (weakest) to 4 (strongest)
56+
/// Calculated using zxcvbn with cipher-specific context
57+
pub password_strength: u8,
58+
/// Number of times password appears in HIBP database
59+
/// None if check_exposed was false in options
60+
pub exposed_count: Option<u32>,
61+
/// Number of times this password appears in the provided cipher list
62+
/// Minimum value is 1 (the cipher itself)
63+
pub reuse_count: u32,
64+
}
65+
66+
#[cfg(feature = "wasm")]
67+
impl wasm_bindgen::__rt::VectorIntoJsValue for CipherRisk {
68+
fn vector_into_jsvalue(
69+
vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>,
70+
) -> wasm_bindgen::JsValue {
71+
wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector)
72+
}
73+
}

0 commit comments

Comments
 (0)