344 lines
10 KiB
Rust
344 lines
10 KiB
Rust
use constant_time_eq::constant_time_eq;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{crypto::verify_password, error::AppError};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: String,
|
|
pub name: Option<String>,
|
|
pub avatar_color: Option<String>,
|
|
pub email: String,
|
|
#[serde(with = "bool_from_int")]
|
|
pub email_verified: bool,
|
|
pub master_password_hash: String,
|
|
pub master_password_hint: Option<String>,
|
|
pub password_salt: Option<String>, // Salt for server-side PBKDF2 (NULL for legacy users)
|
|
pub password_iterations: i32, // Server-side PBKDF2 iterations used for master_password_hash
|
|
pub key: String,
|
|
pub private_key: String,
|
|
pub public_key: String,
|
|
pub kdf_type: i32,
|
|
pub kdf_iterations: i32,
|
|
pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB)
|
|
pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16)
|
|
pub security_stamp: String,
|
|
pub totp_recover: Option<String>, // Recovery code for 2FA
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum PasswordVerification {
|
|
MatchCurrentScheme,
|
|
MatchLegacyScheme,
|
|
Mismatch,
|
|
}
|
|
|
|
impl PasswordVerification {
|
|
pub fn is_valid(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
PasswordVerification::MatchCurrentScheme | PasswordVerification::MatchLegacyScheme
|
|
)
|
|
}
|
|
|
|
pub fn needs_migration(&self) -> bool {
|
|
matches!(self, PasswordVerification::MatchLegacyScheme)
|
|
}
|
|
}
|
|
|
|
impl User {
|
|
pub async fn verify_master_password(
|
|
&self,
|
|
provided_hash: &str,
|
|
) -> Result<PasswordVerification, AppError> {
|
|
if let Some(ref salt) = self.password_salt {
|
|
let is_valid = verify_password(
|
|
provided_hash,
|
|
&self.master_password_hash,
|
|
salt,
|
|
self.password_iterations as u32,
|
|
)
|
|
.await?;
|
|
Ok(if is_valid {
|
|
PasswordVerification::MatchCurrentScheme
|
|
} else {
|
|
PasswordVerification::Mismatch
|
|
})
|
|
} else {
|
|
let is_valid = constant_time_eq(
|
|
self.master_password_hash.as_bytes(),
|
|
provided_hash.as_bytes(),
|
|
);
|
|
|
|
Ok(if is_valid {
|
|
PasswordVerification::MatchLegacyScheme
|
|
} else {
|
|
PasswordVerification::Mismatch
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
mod bool_from_int {
|
|
use serde::{self, Deserialize, Deserializer, Serializer};
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let value = i64::deserialize(deserializer)?;
|
|
match value {
|
|
0 => Ok(false),
|
|
1 => Ok(true),
|
|
_ => Err(serde::de::Error::custom("expected integer 0 or 1")),
|
|
}
|
|
}
|
|
|
|
pub fn serialize<S>(value: &bool, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
if *value {
|
|
serializer.serialize_i64(1)
|
|
} else {
|
|
serializer.serialize_i64(0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// For /accounts/prelogin response
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PreloginResponse {
|
|
pub kdf: i32,
|
|
pub kdf_iterations: i32,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub kdf_memory: Option<i32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub kdf_parallelism: Option<i32>,
|
|
}
|
|
|
|
// For /accounts/register request
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RegisterRequest {
|
|
pub name: Option<String>,
|
|
pub email: String,
|
|
pub master_password_hash: String,
|
|
pub master_password_hint: Option<String>,
|
|
pub user_symmetric_key: String,
|
|
pub user_asymmetric_keys: KeyData,
|
|
pub kdf: i32,
|
|
pub kdf_iterations: i32,
|
|
pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB)
|
|
pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16)
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct KeyData {
|
|
pub public_key: String,
|
|
pub encrypted_private_key: String,
|
|
}
|
|
|
|
/// Request body for password-protected operations (delete account, purge vault, etc.)
|
|
/// Supports both master password hash and OTP verification.
|
|
/// Note: OTP verification is not implemented in this simplified version.
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordOrOtpData {
|
|
#[serde(alias = "MasterPasswordHash")]
|
|
pub master_password_hash: Option<String>,
|
|
#[allow(dead_code)] // OTP verification is not implemented in this simplified version
|
|
pub otp: Option<String>,
|
|
}
|
|
|
|
// For POST /accounts/password request
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ChangePasswordRequest {
|
|
pub master_password_hash: String,
|
|
pub new_master_password_hash: String,
|
|
pub master_password_hint: Option<String>,
|
|
pub key: String,
|
|
}
|
|
|
|
// For POST /accounts/kdf request - Change KDF settings
|
|
//
|
|
// API Format History:
|
|
// - Bitwarden switched to complex format in v2025.10.0
|
|
// - Vaultwarden followed in PR #6458, WITHOUT backward compatibility
|
|
// - We implement backward compatibility to support both formats
|
|
//
|
|
// Supports two formats:
|
|
//
|
|
// 1. Simple/Legacy format (Bitwarden < v2025.10.0, e.g. web vault 2025.07):
|
|
// {
|
|
// "kdf": 0,
|
|
// "kdfIterations": 650000,
|
|
// "kdfMemory": null,
|
|
// "kdfParallelism": null,
|
|
// "key": "...",
|
|
// "masterPasswordHash": "...",
|
|
// "newMasterPasswordHash": "..."
|
|
// }
|
|
//
|
|
// 2. Complex format (Bitwarden >= v2025.10.0, e.g. official client 2025.11.x):
|
|
// {
|
|
// "authenticationData": { "kdf": {...}, "masterPasswordAuthenticationHash": "...", "salt": "..." },
|
|
// "unlockData": { "kdf": {...}, "masterKeyWrappedUserKey": "...", "salt": "..." },
|
|
// "key": "...",
|
|
// "masterPasswordHash": "...",
|
|
// "newMasterPasswordHash": "..."
|
|
// }
|
|
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct KdfParams {
|
|
#[serde(alias = "kdfType")]
|
|
pub kdf_type: i32,
|
|
pub iterations: i32,
|
|
pub memory: Option<i32>,
|
|
pub parallelism: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthenticationData {
|
|
pub salt: String,
|
|
pub kdf: KdfParams,
|
|
pub master_password_authentication_hash: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct UnlockData {
|
|
pub salt: String,
|
|
pub kdf: KdfParams,
|
|
pub master_key_wrapped_user_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ChangeKdfRequest {
|
|
// Common fields (both formats)
|
|
pub key: String,
|
|
pub master_password_hash: String,
|
|
pub new_master_password_hash: String,
|
|
|
|
// Simple format fields (optional)
|
|
pub kdf: Option<i32>,
|
|
pub kdf_iterations: Option<i32>,
|
|
pub kdf_memory: Option<i32>,
|
|
pub kdf_parallelism: Option<i32>,
|
|
|
|
// Complex format fields (optional)
|
|
pub authentication_data: Option<AuthenticationData>,
|
|
pub unlock_data: Option<UnlockData>,
|
|
}
|
|
|
|
impl ChangeKdfRequest {
|
|
/// Extract KDF parameters from either simple or complex format
|
|
pub fn get_kdf_params(&self) -> Option<(i32, i32, Option<i32>, Option<i32>)> {
|
|
// Try complex format first (unlock_data takes precedence)
|
|
if let Some(ref unlock_data) = self.unlock_data {
|
|
return Some((
|
|
unlock_data.kdf.kdf_type,
|
|
unlock_data.kdf.iterations,
|
|
unlock_data.kdf.memory,
|
|
unlock_data.kdf.parallelism,
|
|
));
|
|
}
|
|
|
|
// Fall back to simple format
|
|
if let (Some(kdf), Some(iterations)) = (self.kdf, self.kdf_iterations) {
|
|
return Some((kdf, iterations, self.kdf_memory, self.kdf_parallelism));
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Get the new password hash to store (from authentication_data if available)
|
|
pub fn get_new_password_hash(&self) -> &str {
|
|
if let Some(ref auth_data) = self.authentication_data {
|
|
&auth_data.master_password_authentication_hash
|
|
} else {
|
|
&self.new_master_password_hash
|
|
}
|
|
}
|
|
|
|
/// Get the new encrypted user key (from unlock_data if available, else from key)
|
|
pub fn get_new_key(&self) -> &str {
|
|
if let Some(ref unlock_data) = self.unlock_data {
|
|
&unlock_data.master_key_wrapped_user_key
|
|
} else {
|
|
&self.key
|
|
}
|
|
}
|
|
}
|
|
|
|
// For POST /accounts/key-management/rotate-user-account-keys request
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RotateKeyRequest {
|
|
pub account_unlock_data: RotateAccountUnlockData,
|
|
pub account_keys: RotateAccountKeys,
|
|
pub account_data: RotateAccountData,
|
|
pub old_master_key_authentication_hash: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RotateAccountUnlockData {
|
|
pub master_password_unlock_data: MasterPasswordUnlockData,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct MasterPasswordUnlockData {
|
|
pub kdf_type: i32,
|
|
pub kdf_iterations: i32,
|
|
pub kdf_parallelism: Option<i32>,
|
|
pub kdf_memory: Option<i32>,
|
|
pub email: String,
|
|
pub master_key_authentication_hash: String,
|
|
pub master_key_encrypted_user_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RotateAccountKeys {
|
|
pub user_key_encrypted_account_private_key: String,
|
|
pub account_public_key: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RotateAccountData {
|
|
pub ciphers: Vec<crate::models::cipher::CipherRequestData>,
|
|
pub folders: Vec<RotateFolderData>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RotateFolderData {
|
|
// There is a bug in 2024.3.x which adds a `null` item.
|
|
// To bypass this we allow an Option here, but skip it during the updates
|
|
// See: https://github.com/bitwarden/clients/issues/8453
|
|
pub id: Option<String>,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProfileData {
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AvatarData {
|
|
pub avatar_color: Option<String>,
|
|
}
|