chore: format code

This commit is contained in:
qaz741wsd856 2025-12-06 17:52:31 +00:00
parent 3ba2f09ea3
commit 38b978ca6e
14 changed files with 219 additions and 171 deletions

View file

@ -25,11 +25,13 @@ pub struct Claims {
/// AuthUser extractor - provides (user_id, email) tuple /// AuthUser extractor - provides (user_id, email) tuple
pub struct AuthUser(pub String, pub String); pub struct AuthUser(pub String, pub String);
impl FromRequestParts<Arc<Env>> for Claims impl FromRequestParts<Arc<Env>> for Claims {
{
type Rejection = AppError; type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &Arc<Env>) -> Result<Self, Self::Rejection> { async fn from_request_parts(
parts: &mut Parts,
state: &Arc<Env>,
) -> Result<Self, Self::Rejection> {
// Extract the token from the authorization header // Extract the token from the authorization header
let token = parts let token = parts
.headers .headers
@ -55,11 +57,13 @@ impl FromRequestParts<Arc<Env>> for Claims
} }
} }
impl FromRequestParts<Arc<Env>> for AuthUser impl FromRequestParts<Arc<Env>> for AuthUser {
{
type Rejection = AppError; type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &Arc<Env>) -> Result<Self, Self::Rejection> { async fn from_request_parts(
parts: &mut Parts,
state: &Arc<Env>,
) -> Result<Self, Self::Rejection> {
let claims = Claims::from_request_parts(parts, state).await?; let claims = Claims::from_request_parts(parts, state).await?;
Ok(AuthUser(claims.sub, claims.email)) Ok(AuthUser(claims.sub, claims.email))
} }

View file

@ -154,7 +154,7 @@ pub fn generate_totp_secret() -> Result<String, AppError> {
crypto crypto
.get_random_values_with_array_buffer_view(&secret) .get_random_values_with_array_buffer_view(&secret)
.map_err(|e| AppError::Crypto(format!("Failed to generate TOTP secret: {:?}", e)))?; .map_err(|e| AppError::Crypto(format!("Failed to generate TOTP secret: {:?}", e)))?;
Ok(base32_encode(&secret.to_vec())) Ok(base32_encode(&secret.to_vec()))
} }
@ -164,24 +164,26 @@ async fn hmac_sha1(key: &[u8], data: &[u8]) -> Result<Vec<u8>, AppError> {
// Create algorithm object for HMAC with SHA-1 // Create algorithm object for HMAC with SHA-1
let algorithm = js_sys::Object::new(); let algorithm = js_sys::Object::new();
js_sys::Reflect::set(&algorithm, &JsValue::from_str("name"), &JsValue::from_str("HMAC")) js_sys::Reflect::set(
.map_err(|e| AppError::Crypto(format!("Failed to set algorithm name: {:?}", e)))?; &algorithm,
js_sys::Reflect::set(&algorithm, &JsValue::from_str("hash"), &JsValue::from_str("SHA-1")) &JsValue::from_str("name"),
.map_err(|e| AppError::Crypto(format!("Failed to set hash: {:?}", e)))?; &JsValue::from_str("HMAC"),
)
.map_err(|e| AppError::Crypto(format!("Failed to set algorithm name: {:?}", e)))?;
js_sys::Reflect::set(
&algorithm,
&JsValue::from_str("hash"),
&JsValue::from_str("SHA-1"),
)
.map_err(|e| AppError::Crypto(format!("Failed to set hash: {:?}", e)))?;
// Import the key // Import the key
let key_array = Uint8Array::new_from_slice(key); let key_array = Uint8Array::new_from_slice(key);
let key_usages = js_sys::Array::of1(&JsValue::from_str("sign")); let key_usages = js_sys::Array::of1(&JsValue::from_str("sign"));
let crypto_key = JsFuture::from( let crypto_key = JsFuture::from(
subtle subtle
.import_key_with_object( .import_key_with_object("raw", &key_array, &algorithm, false, &key_usages)
"raw",
&key_array,
&algorithm,
false,
&key_usages,
)
.map_err(|e| AppError::Crypto(format!("HMAC import_key failed: {:?}", e)))?, .map_err(|e| AppError::Crypto(format!("HMAC import_key failed: {:?}", e)))?,
) )
.await .await
@ -191,11 +193,7 @@ async fn hmac_sha1(key: &[u8], data: &[u8]) -> Result<Vec<u8>, AppError> {
let data_array = Uint8Array::new_from_slice(data); let data_array = Uint8Array::new_from_slice(data);
let signature = JsFuture::from( let signature = JsFuture::from(
subtle subtle
.sign_with_str_and_buffer_source( .sign_with_str_and_buffer_source("HMAC", &CryptoKey::from(crypto_key), &data_array)
"HMAC",
&CryptoKey::from(crypto_key),
&data_array,
)
.map_err(|e| AppError::Crypto(format!("HMAC sign failed: {:?}", e)))?, .map_err(|e| AppError::Crypto(format!("HMAC sign failed: {:?}", e)))?,
) )
.await .await
@ -205,42 +203,42 @@ async fn hmac_sha1(key: &[u8], data: &[u8]) -> Result<Vec<u8>, AppError> {
} }
/// Generates a TOTP code for the given secret and time. /// Generates a TOTP code for the given secret and time.
/// ///
/// # Arguments /// # Arguments
/// * `secret` - Base32 encoded secret key /// * `secret` - Base32 encoded secret key
/// * `time_step` - Unix timestamp divided by 30 (or custom period) /// * `time_step` - Unix timestamp divided by 30 (or custom period)
/// ///
/// # Returns /// # Returns
/// * 6-digit TOTP code as a string /// * 6-digit TOTP code as a string
pub async fn generate_totp(secret: &str, time_step: u64) -> Result<String, AppError> { pub async fn generate_totp(secret: &str, time_step: u64) -> Result<String, AppError> {
let decoded_secret = base32_decode(secret)?; let decoded_secret = base32_decode(secret)?;
// Convert time_step to big-endian bytes // Convert time_step to big-endian bytes
let counter = time_step.to_be_bytes(); let counter = time_step.to_be_bytes();
// Compute HMAC-SHA1 // Compute HMAC-SHA1
let hmac = hmac_sha1(&decoded_secret, &counter).await?; let hmac = hmac_sha1(&decoded_secret, &counter).await?;
// Dynamic truncation (RFC 4226) // Dynamic truncation (RFC 4226)
let offset = (hmac[19] & 0x0f) as usize; let offset = (hmac[19] & 0x0f) as usize;
let code = ((hmac[offset] & 0x7f) as u32) << 24 let code = ((hmac[offset] & 0x7f) as u32) << 24
| (hmac[offset + 1] as u32) << 16 | (hmac[offset + 1] as u32) << 16
| (hmac[offset + 2] as u32) << 8 | (hmac[offset + 2] as u32) << 8
| (hmac[offset + 3] as u32); | (hmac[offset + 3] as u32);
// Get 6-digit code // Get 6-digit code
let otp = code % 1_000_000; let otp = code % 1_000_000;
Ok(format!("{:06}", otp)) Ok(format!("{:06}", otp))
} }
/// Validates a TOTP code against a secret. /// Validates a TOTP code against a secret.
/// ///
/// # Arguments /// # Arguments
/// * `code` - The TOTP code to validate /// * `code` - The TOTP code to validate
/// * `secret` - Base32 encoded secret key /// * `secret` - Base32 encoded secret key
/// * `last_used` - The last used time step (for replay protection) /// * `last_used` - The last used time step (for replay protection)
/// * `allow_drift` - Whether to allow 1 time step drift (±30 seconds) /// * `allow_drift` - Whether to allow 1 time step drift (±30 seconds)
/// ///
/// # Returns /// # Returns
/// * `Ok(time_step)` if valid, with the time step that matched /// * `Ok(time_step)` if valid, with the time step that matched
/// * `Err` if invalid /// * `Err` if invalid
@ -257,25 +255,25 @@ pub async fn validate_totp(
let current_time = chrono::Utc::now().timestamp(); let current_time = chrono::Utc::now().timestamp();
let current_step = current_time / 30; let current_step = current_time / 30;
// Check drift range // Check drift range
let steps: i64 = if allow_drift { 1 } else { 0 }; let steps: i64 = if allow_drift { 1 } else { 0 };
for step_offset in -steps..=steps { for step_offset in -steps..=steps {
let time_step = current_step + step_offset; let time_step = current_step + step_offset;
// Skip if this time step was already used (replay protection) // Skip if this time step was already used (replay protection)
if time_step <= last_used { if time_step <= last_used {
continue; continue;
} }
let expected = generate_totp(secret, time_step as u64).await?; let expected = generate_totp(secret, time_step as u64).await?;
if constant_time_eq(code.as_bytes(), expected.as_bytes()) { if constant_time_eq(code.as_bytes(), expected.as_bytes()) {
return Ok(time_step); return Ok(time_step);
} }
} }
Err(AppError::Unauthorized(format!( Err(AppError::Unauthorized(format!(
"Invalid TOTP code. Server time: {}", "Invalid TOTP code. Server time: {}",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
@ -289,7 +287,7 @@ pub fn generate_recovery_code() -> Result<String, AppError> {
crypto crypto
.get_random_values_with_array_buffer_view(&bytes) .get_random_values_with_array_buffer_view(&bytes)
.map_err(|e| AppError::Crypto(format!("Failed to generate recovery code: {:?}", e)))?; .map_err(|e| AppError::Crypto(format!("Failed to generate recovery code: {:?}", e)))?;
Ok(base32_encode(&bytes.to_vec())) Ok(base32_encode(&bytes.to_vec()))
} }

View file

@ -62,7 +62,9 @@ impl IntoResponse for AppError {
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
format!("Crypto error: {}", msg), format!("Crypto error: {}", msg),
), ),
AppError::JsonWebToken(_) => (StatusCode::UNAUTHORIZED, "Invalid token".to_string()), AppError::JsonWebToken(_) => {
(StatusCode::UNAUTHORIZED, "Invalid token".to_string())
}
AppError::Internal => ( AppError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(), "Internal server error".to_string(),

View file

@ -1,8 +1,4 @@
use axum::{ use axum::{extract::State, http::HeaderMap, Json};
extract::State,
http::HeaderMap,
Json,
};
use chrono::Utc; use chrono::Utc;
use glob_match::glob_match; use glob_match::glob_match;
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -21,7 +17,8 @@ use crate::{
sync::Profile, sync::Profile,
user::{ user::{
AvatarData, ChangeKdfRequest, ChangePasswordRequest, MasterPasswordUnlockData, AvatarData, ChangeKdfRequest, ChangePasswordRequest, MasterPasswordUnlockData,
PasswordOrOtpData, PreloginResponse, ProfileData, RegisterRequest, RotateKeyRequest, User, PasswordOrOtpData, PreloginResponse, ProfileData, RegisterRequest, RotateKeyRequest,
User,
}, },
}, },
}; };
@ -148,7 +145,9 @@ pub async fn prelogin(
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let stmt = db.prepare("SELECT kdf_type, kdf_iterations, kdf_memory, kdf_parallelism FROM users WHERE email = ?1"); let stmt = db.prepare(
"SELECT kdf_type, kdf_iterations, kdf_memory, kdf_parallelism FROM users WHERE email = ?1",
);
let query = stmt.bind(&[email.into()])?; let query = stmt.bind(&[email.into()])?;
let row: Option<Value> = query.first(None).await.map_err(|_| AppError::Database)?; let row: Option<Value> = query.first(None).await.map_err(|_| AppError::Database)?;
@ -616,7 +615,11 @@ pub async fn post_rotatekey(
// All personal ciphers must have an id for key rotation // All personal ciphers must have an id for key rotation
if personal_ciphers.len() != request_cipher_ids.len() { if personal_ciphers.len() != request_cipher_ids.len() {
log::error!("All ciphers must have an id for key rotation: {:?} != {:?}", personal_ciphers.len(), request_cipher_ids.len()); log::error!(
"All ciphers must have an id for key rotation: {:?} != {:?}",
personal_ciphers.len(),
request_cipher_ids.len()
);
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"All ciphers must have an id for key rotation".to_string(), "All ciphers must have an id for key rotation".to_string(),
)); ));
@ -676,7 +679,13 @@ pub async fn post_rotatekey(
.unwrap_or(0) as usize; .unwrap_or(0) as usize;
if db_cipher_count != request_cipher_ids.len() || db_folder_count != request_folder_ids.len() { if db_cipher_count != request_cipher_ids.len() || db_folder_count != request_folder_ids.len() {
log::error!("Cipher or folder count mismatch in rotation request: {:?} != {:?} or {:?} != {:?}", db_cipher_count, request_cipher_ids.len(), db_folder_count, request_folder_ids.len()); log::error!(
"Cipher or folder count mismatch in rotation request: {:?} != {:?} or {:?} != {:?}",
db_cipher_count,
request_cipher_ids.len(),
db_folder_count,
request_folder_ids.len()
);
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"All existing ciphers and folders must be included in the rotation".to_string(), "All existing ciphers and folders must be included in the rotation".to_string(),
)); ));
@ -687,7 +696,11 @@ pub async fn post_rotatekey(
let has_missing_folders = !validation_results[3].results::<Value>()?.is_empty(); let has_missing_folders = !validation_results[3].results::<Value>()?.is_empty();
if has_missing_ciphers || has_missing_folders { if has_missing_ciphers || has_missing_folders {
log::error!("Missing ciphers or folders in rotation request: {:?} or {:?}", has_missing_ciphers, has_missing_folders); log::error!(
"Missing ciphers or folders in rotation request: {:?} or {:?}",
has_missing_ciphers,
has_missing_folders
);
return Err(AppError::BadRequest( return Err(AppError::BadRequest(
"All existing ciphers and folders must be included in the rotation".to_string(), "All existing ciphers and folders must be included in the rotation".to_string(),
)); ));

View file

@ -2,7 +2,7 @@ use super::get_batch_size;
use axum::{extract::State, Json}; use axum::{extract::State, Json};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use log; // Used for warning logs on parse failures use log; // Used for warning logs on parse failures
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -11,7 +11,10 @@ use worker::{query, D1PreparedStatement, Env};
use crate::auth::Claims; use crate::auth::Claims;
use crate::db; use crate::db;
use crate::error::AppError; use crate::error::AppError;
use crate::models::cipher::{Cipher, CipherData, CipherDBModel, CipherListResponse, CipherRequestData, CreateCipherRequest, MoveCipherData, PartialCipherData}; use crate::models::cipher::{
Cipher, CipherDBModel, CipherData, CipherListResponse, CipherRequestData, CreateCipherRequest,
MoveCipherData, PartialCipherData,
};
use crate::models::user::{PasswordOrOtpData, User}; use crate::models::user::{PasswordOrOtpData, User};
use axum::extract::Path; use axum::extract::Path;
@ -153,7 +156,7 @@ pub async fn update_cipher(
Err(err) => log::warn!("Error parsing lastKnownRevisionDate '{}': {}", dt, err), Err(err) => log::warn!("Error parsing lastKnownRevisionDate '{}': {}", dt, err),
} }
} }
let cipher_data_req = payload; let cipher_data_req = payload;
let cipher_data = CipherData { let cipher_data = CipherData {
@ -515,7 +518,9 @@ pub async fn restore_ciphers_bulk(
let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?; let ids_json = serde_json::to_string(&ids).map_err(|_| AppError::Internal)?;
let restored_ciphers: Vec<Cipher> = db let restored_ciphers: Vec<Cipher> = db
.prepare("SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))") .prepare(
"SELECT * FROM ciphers WHERE user_id = ?1 AND id IN (SELECT value FROM json_each(?2))",
)
.bind(&[claims.sub.clone().into(), ids_json.into()])? .bind(&[claims.sub.clone().into(), ids_json.into()])?
.all() .all()
.await? .await?

View file

@ -31,7 +31,7 @@ pub async fn config(
// feature_states.insert("unauth-ui-refresh".to_string(), true); // feature_states.insert("unauth-ui-refresh".to_string(), true);
// feature_states.insert("enable-pm-flight-recorder".to_string(), true); // feature_states.insert("enable-pm-flight-recorder".to_string(), true);
// feature_states.insert("mobile-error-reporting".to_string(), true); // feature_states.insert("mobile-error-reporting".to_string(), true);
let disable_user_registration = get_disable_user_registration(&env); let disable_user_registration = get_disable_user_registration(&env);
Json(json!({ Json(json!({

View file

@ -9,9 +9,9 @@ use worker::{query, Env};
use crate::{ use crate::{
auth::Claims, auth::Claims,
crypto::{ct_eq, generate_salt, hash_password_for_storage, validate_totp}, crypto::{ct_eq, generate_salt, hash_password_for_storage, validate_totp},
handlers::allow_totp_drift,
db, db,
error::AppError, error::AppError,
handlers::allow_totp_drift,
models::twofactor::{RememberTokenData, TwoFactor, TwoFactorType}, models::twofactor::{RememberTokenData, TwoFactor, TwoFactorType},
models::user::User, models::user::User,
}; };
@ -49,9 +49,17 @@ pub struct TokenRequest {
// 2FA fields // 2FA fields
#[serde(rename = "twoFactorToken")] #[serde(rename = "twoFactorToken")]
two_factor_token: Option<String>, two_factor_token: Option<String>,
#[serde(rename = "twoFactorProvider", default, deserialize_with = "deserialize_trimmed_i32")] #[serde(
rename = "twoFactorProvider",
default,
deserialize_with = "deserialize_trimmed_i32"
)]
two_factor_provider: Option<i32>, two_factor_provider: Option<i32>,
#[serde(rename = "twoFactorRemember", default, deserialize_with = "deserialize_trimmed_i32")] #[serde(
rename = "twoFactorRemember",
default,
deserialize_with = "deserialize_trimmed_i32"
)]
two_factor_remember: Option<i32>, two_factor_remember: Option<i32>,
#[serde(rename = "deviceIdentifier")] #[serde(rename = "deviceIdentifier")]
device_identifier: Option<String>, device_identifier: Option<String>,
@ -235,7 +243,9 @@ pub async fn token(
Some(code) => code, Some(code) => code,
None => { None => {
// Return 2FA required error // Return 2FA required error
return Err(AppError::TwoFactorRequired(json_err_twofactor(&twofactor_ids))); return Err(AppError::TwoFactorRequired(json_err_twofactor(
&twofactor_ids,
)));
} }
}; };
@ -250,12 +260,13 @@ pub async fn token(
let tf = selected_twofactor.ok_or_else(|| { let tf = selected_twofactor.ok_or_else(|| {
AppError::BadRequest("TOTP not configured".to_string()) AppError::BadRequest("TOTP not configured".to_string())
})?; })?;
// Validate TOTP code // Validate TOTP code
let allow_drift = allow_totp_drift(&env); let allow_drift = allow_totp_drift(&env);
let new_last_used = let new_last_used =
validate_totp(twofactor_code, &tf.data, tf.last_used, allow_drift).await?; validate_totp(twofactor_code, &tf.data, tf.last_used, allow_drift)
.await?;
// Update last_used // Update last_used
query!( query!(
&db, &db,
@ -276,19 +287,21 @@ pub async fn token(
let remember_tf = twofactors let remember_tf = twofactors
.iter() .iter()
.find(|tf| tf.atype == TwoFactorType::Remember as i32); .find(|tf| tf.atype == TwoFactorType::Remember as i32);
if let Some(tf) = remember_tf { if let Some(tf) = remember_tf {
// Parse stored remember tokens as JSON // Parse stored remember tokens as JSON
let mut token_data = RememberTokenData::from_json(&tf.data); let mut token_data = RememberTokenData::from_json(&tf.data);
// Remove expired tokens first // Remove expired tokens first
token_data.remove_expired(); token_data.remove_expired();
// Validate the provided token // Validate the provided token
if !token_data.validate(device_id, twofactor_code) { if !token_data.validate(device_id, twofactor_code) {
return Err(AppError::TwoFactorRequired(json_err_twofactor(&twofactor_ids))); return Err(AppError::TwoFactorRequired(json_err_twofactor(
&twofactor_ids,
)));
} }
// Update database with cleaned tokens (remove expired) // Update database with cleaned tokens (remove expired)
let updated_data = token_data.to_json(); let updated_data = token_data.to_json();
query!( query!(
@ -301,33 +314,35 @@ pub async fn token(
.run() .run()
.await .await
.map_err(|_| AppError::Database)?; .map_err(|_| AppError::Database)?;
// Remember token valid, proceed with login // Remember token valid, proceed with login
} else { } else {
return Err(AppError::TwoFactorRequired(json_err_twofactor(&twofactor_ids))); return Err(AppError::TwoFactorRequired(json_err_twofactor(
&twofactor_ids,
)));
} }
} else { } else {
return Err(AppError::TwoFactorRequired(json_err_twofactor(&twofactor_ids))); return Err(AppError::TwoFactorRequired(json_err_twofactor(
&twofactor_ids,
)));
} }
} }
Some(TwoFactorType::RecoveryCode) => { Some(TwoFactorType::RecoveryCode) => {
// Check recovery code // Check recovery code
if let Some(ref stored_code) = user.totp_recover { if let Some(ref stored_code) = user.totp_recover {
if !ct_eq(&stored_code.to_uppercase(), &twofactor_code.to_uppercase()) { if !ct_eq(&stored_code.to_uppercase(), &twofactor_code.to_uppercase()) {
return Err(AppError::BadRequest("Recovery code is incorrect".to_string())); return Err(AppError::BadRequest(
"Recovery code is incorrect".to_string(),
));
} }
// Delete all 2FA and clear recovery code // Delete all 2FA and clear recovery code
query!( query!(&db, "DELETE FROM twofactor WHERE user_uuid = ?1", &user.id)
&db, .map_err(|_| AppError::Database)?
"DELETE FROM twofactor WHERE user_uuid = ?1", .run()
&user.id .await
) .map_err(|_| AppError::Database)?;
.map_err(|_| AppError::Database)?
.run()
.await
.map_err(|_| AppError::Database)?;
query!( query!(
&db, &db,
"UPDATE users SET totp_recover = NULL WHERE id = ?1", "UPDATE users SET totp_recover = NULL WHERE id = ?1",
@ -338,11 +353,15 @@ pub async fn token(
.await .await
.map_err(|_| AppError::Database)?; .map_err(|_| AppError::Database)?;
} else { } else {
return Err(AppError::BadRequest("Recovery code is incorrect".to_string())); return Err(AppError::BadRequest(
"Recovery code is incorrect".to_string(),
));
} }
} }
_ => { _ => {
return Err(AppError::BadRequest("Invalid two factor provider".to_string())); return Err(AppError::BadRequest(
"Invalid two factor provider".to_string(),
));
} }
} }
@ -350,24 +369,24 @@ pub async fn token(
if payload.two_factor_remember == Some(1) { if payload.two_factor_remember == Some(1) {
if let Some(ref device_id) = payload.device_identifier { if let Some(ref device_id) = payload.device_identifier {
let remember_token = uuid::Uuid::new_v4().to_string(); let remember_token = uuid::Uuid::new_v4().to_string();
// Load existing remember tokens or create new // Load existing remember tokens or create new
let remember_tf = twofactors let remember_tf = twofactors
.iter() .iter()
.find(|tf| tf.atype == TwoFactorType::Remember as i32); .find(|tf| tf.atype == TwoFactorType::Remember as i32);
let mut token_data = remember_tf let mut token_data = remember_tf
.map(|tf| RememberTokenData::from_json(&tf.data)) .map(|tf| RememberTokenData::from_json(&tf.data))
.unwrap_or_default(); .unwrap_or_default();
// Remove expired tokens first // Remove expired tokens first
token_data.remove_expired(); token_data.remove_expired();
// Add/update token for this device // Add/update token for this device
token_data.upsert(device_id.clone(), remember_token.clone()); token_data.upsert(device_id.clone(), remember_token.clone());
let json_data = token_data.to_json(); let json_data = token_data.to_json();
// Store or update remember token // Store or update remember token
query!( query!(
&db, &db,
@ -383,7 +402,7 @@ pub async fn token(
.run() .run()
.await .await
.map_err(|_| AppError::Database)?; .map_err(|_| AppError::Database)?;
two_factor_remember_token = Some(remember_token); two_factor_remember_token = Some(remember_token);
} }
} }
@ -467,7 +486,7 @@ fn json_err_twofactor(providers: &[i32]) -> Value {
// Add provider-specific info // Add provider-specific info
for provider in providers { for provider in providers {
result["TwoFactorProviders2"][provider.to_string()] = Value::Null; result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
// TOTP doesn't need any additional info // TOTP doesn't need any additional info
// Other providers like Email, WebAuthn etc. would add their info here // Other providers like Email, WebAuthn etc. would add their info here
} }

View file

@ -38,10 +38,8 @@ pub async fn import_data(
.await? .await?
.results::<FolderIdRow>()?; .results::<FolderIdRow>()?;
let existing_folders: HashSet<String> = existing_folder_rows let existing_folders: HashSet<String> =
.into_iter() existing_folder_rows.into_iter().map(|row| row.id).collect();
.map(|row| row.id)
.collect();
// Process folders and build the folder_id list // Process folders and build the folder_id list
let mut folder_statements: Vec<D1PreparedStatement> = Vec::new(); let mut folder_statements: Vec<D1PreparedStatement> = Vec::new();
@ -112,7 +110,8 @@ pub async fn import_data(
// Build the relations map: cipher_index -> folder_index // Build the relations map: cipher_index -> folder_index
// Each cipher can only be in one folder at a time // Each cipher can only be in one folder at a time
let mut relations_map: HashMap<usize, usize> = HashMap::with_capacity(data.folder_relationships.len()); let mut relations_map: HashMap<usize, usize> =
HashMap::with_capacity(data.folder_relationships.len());
for relation in data.folder_relationships { for relation in data.folder_relationships {
relations_map.insert(relation.key, relation.value); relations_map.insert(relation.key, relation.value);
} }

View file

@ -21,8 +21,7 @@ use crate::{
pub async fn get_twofactor( pub async fn get_twofactor(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
AuthUser(user_id, _): AuthUser, AuthUser(user_id, _): AuthUser,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
let twofactors: Vec<Value> = db let twofactors: Vec<Value> = db
@ -50,8 +49,7 @@ pub async fn get_authenticator(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
AuthUser(user_id, _): AuthUser, AuthUser(user_id, _): AuthUser,
Json(data): Json<PasswordOrOtpData>, Json(data): Json<PasswordOrOtpData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
// Verify master password // Verify master password
@ -69,7 +67,10 @@ pub async fn get_authenticator(
// Check if TOTP is already configured // Check if TOTP is already configured
let existing: Option<Value> = db let existing: Option<Value> = db
.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype = ?2") .prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype = ?2")
.bind(&[user_id.clone().into(), (TwoFactorType::Authenticator as i32).into()])? .bind(&[
user_id.clone().into(),
(TwoFactorType::Authenticator as i32).into(),
])?
.first(None) .first(None)
.await .await
.map_err(|_| AppError::Database)?; .map_err(|_| AppError::Database)?;
@ -95,8 +96,7 @@ pub async fn activate_authenticator(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
AuthUser(user_id, _): AuthUser, AuthUser(user_id, _): AuthUser,
Json(data): Json<EnableAuthenticatorData>, Json(data): Json<EnableAuthenticatorData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
// Verify master password // Verify master password
@ -129,7 +129,10 @@ pub async fn activate_authenticator(
// Check if TOTP is already configured - reuse existing record for replay protection // Check if TOTP is already configured - reuse existing record for replay protection
let existing: Option<TwoFactor> = db let existing: Option<TwoFactor> = db
.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype = ?2") .prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype = ?2")
.bind(&[user_id.clone().into(), (TwoFactorType::Authenticator as i32).into()])? .bind(&[
user_id.clone().into(),
(TwoFactorType::Authenticator as i32).into(),
])?
.first(None) .first(None)
.await .await
.map_err(|_| AppError::Database)? .map_err(|_| AppError::Database)?
@ -191,8 +194,7 @@ pub async fn activate_authenticator_put(
state: State<Arc<Env>>, state: State<Arc<Env>>,
auth_user: AuthUser, auth_user: AuthUser,
json: Json<EnableAuthenticatorData>, json: Json<EnableAuthenticatorData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
activate_authenticator(state, auth_user, json).await activate_authenticator(state, auth_user, json).await
} }
@ -202,8 +204,7 @@ pub async fn disable_twofactor(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
AuthUser(user_id, _): AuthUser, AuthUser(user_id, _): AuthUser,
Json(data): Json<DisableTwoFactorData>, Json(data): Json<DisableTwoFactorData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
// Verify master password // Verify master password
@ -250,7 +251,6 @@ pub async fn disable_twofactor(
}))) })))
} }
/// DELETE /api/two-factor/authenticator - Disable TOTP with key verification /// DELETE /api/two-factor/authenticator - Disable TOTP with key verification
#[worker::send] #[worker::send]
pub async fn disable_authenticator( pub async fn disable_authenticator(
@ -304,17 +304,17 @@ pub async fn disable_authenticator(
)); ));
} }
query!( query!(&db, "DELETE FROM twofactor WHERE uuid = ?1", &tf.uuid)
&db, .map_err(|_| AppError::Database)?
"DELETE FROM twofactor WHERE uuid = ?1", .run()
&tf.uuid .await
) .map_err(|_| AppError::Database)?;
.map_err(|_| AppError::Database)?
.run()
.await
.map_err(|_| AppError::Database)?;
log::info!("User {} disabled authenticator (2FA type {})", user_id, data.r#type); log::info!(
"User {} disabled authenticator (2FA type {})",
user_id,
data.r#type
);
clear_recovery_if_no_twofactor(&db, &user_id).await?; clear_recovery_if_no_twofactor(&db, &user_id).await?;
@ -331,8 +331,7 @@ pub async fn disable_twofactor_put(
state: State<Arc<Env>>, state: State<Arc<Env>>,
auth_user: AuthUser, auth_user: AuthUser,
json: Json<DisableTwoFactorData>, json: Json<DisableTwoFactorData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
disable_twofactor(state, auth_user, json).await disable_twofactor(state, auth_user, json).await
} }
@ -342,8 +341,7 @@ pub async fn get_recover(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
AuthUser(user_id, _): AuthUser, AuthUser(user_id, _): AuthUser,
Json(data): Json<PasswordOrOtpData>, Json(data): Json<PasswordOrOtpData>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
// Verify master password // Verify master password
@ -369,8 +367,7 @@ pub async fn get_recover(
pub async fn recover( pub async fn recover(
State(env): State<Arc<Env>>, State(env): State<Arc<Env>>,
Json(data): Json<RecoverTwoFactor>, Json(data): Json<RecoverTwoFactor>,
) ) -> Result<Json<Value>, AppError> {
-> Result<Json<Value>, AppError> {
let db = db::get_db(&env)?; let db = db::get_db(&env)?;
// Get user by email // Get user by email
@ -384,7 +381,9 @@ pub async fn recover(
let user: User = serde_json::from_value(user_value).map_err(|_| AppError::Internal)?; let user: User = serde_json::from_value(user_value).map_err(|_| AppError::Internal)?;
// Verify master password // Verify master password
let verification = user.verify_master_password(&data.master_password_hash).await?; let verification = user
.verify_master_password(&data.master_password_hash)
.await?;
if !verification.is_valid() { if !verification.is_valid() {
return Err(AppError::Unauthorized( return Err(AppError::Unauthorized(
"Username or password is incorrect".to_string(), "Username or password is incorrect".to_string(),
@ -393,7 +392,10 @@ pub async fn recover(
// Check recovery code (case-insensitive) // Check recovery code (case-insensitive)
let is_valid = user.totp_recover.as_ref().map_or(false, |stored_code| { let is_valid = user.totp_recover.as_ref().map_or(false, |stored_code| {
ct_eq(&stored_code.to_uppercase(), &data.recovery_code.to_uppercase()) ct_eq(
&stored_code.to_uppercase(),
&data.recovery_code.to_uppercase(),
)
}); });
if !is_valid { if !is_valid {
@ -403,15 +405,11 @@ pub async fn recover(
} }
// Delete all 2FA methods // Delete all 2FA methods
query!( query!(&db, "DELETE FROM twofactor WHERE user_uuid = ?1", &user.id)
&db, .map_err(|_| AppError::Database)?
"DELETE FROM twofactor WHERE user_uuid = ?1", .run()
&user.id .await
) .map_err(|_| AppError::Database)?;
.map_err(|_| AppError::Database)?
.run()
.await
.map_err(|_| AppError::Database)?;
// Clear recovery code // Clear recovery code
query!( query!(
@ -448,8 +446,7 @@ async fn validate_password_or_otp(user: &User, data: &PasswordOrOtpData) -> Resu
async fn generate_recovery_code_for_user( async fn generate_recovery_code_for_user(
db: &worker::D1Database, db: &worker::D1Database,
user_id: &str, user_id: &str,
) ) -> Result<(), AppError> {
-> Result<(), AppError> {
// Check if recovery code already exists // Check if recovery code already exists
let user_value: Value = db let user_value: Value = db
.prepare("SELECT totp_recover FROM users WHERE id = ?1") .prepare("SELECT totp_recover FROM users WHERE id = ?1")
@ -482,7 +479,10 @@ async fn generate_recovery_code_for_user(
} }
/// Clear recovery code when no real 2FA providers remain. /// Clear recovery code when no real 2FA providers remain.
async fn clear_recovery_if_no_twofactor(db: &worker::D1Database, user_id: &str) -> Result<(), AppError> { async fn clear_recovery_if_no_twofactor(
db: &worker::D1Database,
user_id: &str,
) -> Result<(), AppError> {
let remaining: Vec<TwoFactor> = db let remaining: Vec<TwoFactor> = db
.prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype < 1000 AND atype != ?2") .prepare("SELECT * FROM twofactor WHERE user_uuid = ?1 AND atype < 1000 AND atype != ?2")
.bind(&[ .bind(&[
@ -496,11 +496,15 @@ async fn clear_recovery_if_no_twofactor(db: &worker::D1Database, user_id: &str)
.map_err(|_| AppError::Database)?; .map_err(|_| AppError::Database)?;
if remaining.is_empty() { if remaining.is_empty() {
query!(db, "UPDATE users SET totp_recover = NULL WHERE id = ?1", user_id) query!(
.map_err(|_| AppError::Database)? db,
.run() "UPDATE users SET totp_recover = NULL WHERE id = ?1",
.await user_id
.map_err(|_| AppError::Database)?; )
.map_err(|_| AppError::Database)?
.run()
.await
.map_err(|_| AppError::Database)?;
} }
Ok(()) Ok(())

View file

@ -183,10 +183,13 @@ impl Serialize for Cipher {
response_map.insert("edit".to_string(), json!(self.edit)); response_map.insert("edit".to_string(), json!(self.edit));
response_map.insert("viewPassword".to_string(), json!(self.view_password)); response_map.insert("viewPassword".to_string(), json!(self.view_password));
// new key "permissions" used by clients since v2025.6.0 // new key "permissions" used by clients since v2025.6.0
response_map.insert("permissions". to_string(), json! ({ response_map.insert(
"delete": self.edit, // if edit is true, allow delete "permissions".to_string(),
"restore": self.edit, // if edit is true, allow restore json! ({
})); "delete": self.edit, // if edit is true, allow delete
"restore": self.edit, // if edit is true, allow restore
}),
);
response_map.insert( response_map.insert(
"organizationUseTotp".to_string(), "organizationUseTotp".to_string(),
json!(self.organization_use_totp), json!(self.organization_use_totp),
@ -336,4 +339,4 @@ pub struct CipherListResponse {
pub struct PartialCipherData { pub struct PartialCipherData {
pub folder_id: Option<String>, pub folder_id: Option<String>,
pub favorite: bool, pub favorite: bool,
} }

View file

@ -1,6 +1,6 @@
pub mod user;
pub mod sync;
pub mod cipher; pub mod cipher;
pub mod folder; pub mod folder;
pub mod import; pub mod import;
pub mod sync;
pub mod twofactor; pub mod twofactor;
pub mod user;

View file

@ -169,7 +169,8 @@ impl RememberTokenData {
pub fn remove_expired(&mut self) { pub fn remove_expired(&mut self) {
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds(); let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds();
self.tokens.retain(|t| now - t.created_at < expiration_seconds); self.tokens
.retain(|t| now - t.created_at < expiration_seconds);
} }
/// Validate a token for a specific device /// Validate a token for a specific device
@ -177,11 +178,9 @@ impl RememberTokenData {
pub fn validate(&self, device_id: &str, token: &str) -> bool { pub fn validate(&self, device_id: &str, token: &str) -> bool {
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds(); let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds();
self.tokens.iter().any(|t| { self.tokens.iter().any(|t| {
t.device_id == device_id t.device_id == device_id && t.token == token && now - t.created_at < expiration_seconds
&& t.token == token
&& now - t.created_at < expiration_seconds
}) })
} }
@ -190,7 +189,7 @@ impl RememberTokenData {
pub fn upsert(&mut self, device_id: String, token: String) { pub fn upsert(&mut self, device_id: String, token: String) {
// Remove existing token for this device // Remove existing token for this device
self.tokens.retain(|t| t.device_id != device_id); self.tokens.retain(|t| t.device_id != device_id);
// Add new token // Add new token
self.tokens.push(RememberTokenEntry { self.tokens.push(RememberTokenEntry {
device_id, device_id,
@ -203,6 +202,8 @@ impl RememberTokenData {
pub fn has_valid_tokens(&self) -> bool { pub fn has_valid_tokens(&self) -> bool {
let now = Utc::now().timestamp(); let now = Utc::now().timestamp();
let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds(); let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds();
self.tokens.iter().any(|t| now - t.created_at < expiration_seconds) self.tokens
.iter()
.any(|t| now - t.created_at < expiration_seconds)
} }
} }

View file

@ -19,7 +19,7 @@ pub struct User {
pub public_key: String, pub public_key: String,
pub kdf_type: i32, pub kdf_type: i32,
pub kdf_iterations: i32, pub kdf_iterations: i32,
pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB) pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB)
pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16) pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16)
pub security_stamp: String, pub security_stamp: String,
pub totp_recover: Option<String>, // Recovery code for 2FA pub totp_recover: Option<String>, // Recovery code for 2FA
@ -125,7 +125,7 @@ pub struct RegisterRequest {
pub user_asymmetric_keys: KeyData, pub user_asymmetric_keys: KeyData,
pub kdf: i32, pub kdf: i32,
pub kdf_iterations: i32, pub kdf_iterations: i32,
pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB) pub kdf_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB)
pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16) pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16)
} }

View file

@ -5,7 +5,10 @@ use axum::{
use std::sync::Arc; use std::sync::Arc;
use worker::Env; use worker::Env;
use crate::handlers::{accounts, ciphers, config, devices, emergency_access, folders, identity, import, sync, twofactor, webauth}; use crate::handlers::{
accounts, ciphers, config, devices, emergency_access, folders, identity, import, sync,
twofactor, webauth,
};
pub fn api_router(env: Env) -> Router { pub fn api_router(env: Env) -> Router {
let app_state = Arc::new(env); let app_state = Arc::new(env);
@ -48,7 +51,10 @@ pub fn api_router(env: Env) -> Router {
.route("/api/ciphers/create", post(ciphers::create_cipher)) .route("/api/ciphers/create", post(ciphers::create_cipher))
.route("/api/ciphers/import", post(import::import_data)) .route("/api/ciphers/import", post(import::import_data))
.route("/api/ciphers/{id}", get(ciphers::get_cipher)) .route("/api/ciphers/{id}", get(ciphers::get_cipher))
.route("/api/ciphers/{id}/details", get(ciphers::get_cipher_details)) .route(
"/api/ciphers/{id}/details",
get(ciphers::get_cipher_details),
)
.route("/api/ciphers/{id}", put(ciphers::update_cipher)) .route("/api/ciphers/{id}", put(ciphers::update_cipher))
.route("/api/ciphers/{id}", post(ciphers::update_cipher)) .route("/api/ciphers/{id}", post(ciphers::update_cipher))
// Cipher soft delete (PUT sets deleted_at timestamp) // Cipher soft delete (PUT sets deleted_at timestamp)
@ -126,10 +132,7 @@ pub fn api_router(env: Env) -> Router {
post(devices::post_clear_device_token), post(devices::post_clear_device_token),
) )
// WebAuthn (stub - prevents 404 errors, passkeys not supported) // WebAuthn (stub - prevents 404 errors, passkeys not supported)
.route( .route("/api/webauthn", get(webauth::get_webauthn_credentials))
"/api/webauthn",
get(webauth::get_webauthn_credentials),
)
// Two-factor authentication // Two-factor authentication
.route("/api/two-factor", get(twofactor::get_twofactor)) .route("/api/two-factor", get(twofactor::get_twofactor))
.route( .route(
@ -156,10 +159,7 @@ pub fn api_router(env: Env) -> Router {
"/api/two-factor/disable", "/api/two-factor/disable",
put(twofactor::disable_twofactor_put), put(twofactor::disable_twofactor_put),
) )
.route( .route("/api/two-factor/get-recover", post(twofactor::get_recover))
"/api/two-factor/get-recover",
post(twofactor::get_recover),
)
.route("/api/two-factor/recover", post(twofactor::recover)) .route("/api/two-factor/recover", post(twofactor::recover))
.with_state(app_state) .with_state(app_state)
} }