Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion Cargo.lock

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

7 changes: 5 additions & 2 deletions crates/bitwarden-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ uniffi = [
"bitwarden-collections/uniffi",
"bitwarden-core/uniffi",
"bitwarden-crypto/uniffi",
"dep:uniffi"
"dep:uniffi",
] # Uniffi bindings
wasm = [
"bitwarden-collections/wasm",
"bitwarden-core/wasm",
"bitwarden-encoding/wasm",
"dep:tsify",
"dep:wasm-bindgen",
"dep:wasm-bindgen-futures"
"dep:wasm-bindgen-futures",
] # WASM support

[dependencies]
Expand All @@ -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
75 changes: 75 additions & 0 deletions crates/bitwarden-vault/src/cipher/cipher_risk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::collections::HashMap;

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

use crate::CipherId;

/// 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,
/// Number of times password appears in HIBP database.
/// None if check_exposed was false in options.
pub exposed_count: Option<u32>,
/// Number of times this password appears in the provided cipher list.
/// Minimum value is 1 (the cipher itself).
pub reuse_count: 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