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
pub struct AuthUser(pub String, pub String);
impl FromRequestParts<Arc<Env>> for Claims
{
impl FromRequestParts<Arc<Env>> for Claims {
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
let token = parts
.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;
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?;
Ok(AuthUser(claims.sub, claims.email))
}

View file

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

View file

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

View file

@ -1,8 +1,4 @@
use axum::{
extract::State,
http::HeaderMap,
Json,
};
use axum::{extract::State, http::HeaderMap, Json};
use chrono::Utc;
use glob_match::glob_match;
use serde_json::{json, Value};
@ -21,7 +17,8 @@ use crate::{
sync::Profile,
user::{
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 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 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
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(
"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;
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(
"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();
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(
"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 chrono::{DateTime, Utc};
use log; // Used for warning logs on parse failures
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
use uuid::Uuid;
@ -11,7 +11,10 @@ use worker::{query, D1PreparedStatement, Env};
use crate::auth::Claims;
use crate::db;
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 axum::extract::Path;
@ -153,7 +156,7 @@ pub async fn update_cipher(
Err(err) => log::warn!("Error parsing lastKnownRevisionDate '{}': {}", dt, err),
}
}
let cipher_data_req = payload;
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 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()])?
.all()
.await?

View file

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

View file

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

View file

@ -38,10 +38,8 @@ pub async fn import_data(
.await?
.results::<FolderIdRow>()?;
let existing_folders: HashSet<String> = existing_folder_rows
.into_iter()
.map(|row| row.id)
.collect();
let existing_folders: HashSet<String> =
existing_folder_rows.into_iter().map(|row| row.id).collect();
// Process folders and build the folder_id list
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
// 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 {
relations_map.insert(relation.key, relation.value);
}

View file

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

View file

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

View file

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

View file

@ -169,7 +169,8 @@ impl RememberTokenData {
pub fn remove_expired(&mut self) {
let now = Utc::now().timestamp();
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
@ -177,11 +178,9 @@ impl RememberTokenData {
pub fn validate(&self, device_id: &str, token: &str) -> bool {
let now = Utc::now().timestamp();
let expiration_seconds = Duration::days(REMEMBER_TOKEN_EXPIRATION_DAYS).num_seconds();
self.tokens.iter().any(|t| {
t.device_id == device_id
&& t.token == token
&& now - t.created_at < expiration_seconds
t.device_id == device_id && 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) {
// Remove existing token for this device
self.tokens.retain(|t| t.device_id != device_id);
// Add new token
self.tokens.push(RememberTokenEntry {
device_id,
@ -203,6 +202,8 @@ impl RememberTokenData {
pub fn has_valid_tokens(&self) -> bool {
let now = Utc::now().timestamp();
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 kdf_type: 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 security_stamp: String,
pub totp_recover: Option<String>, // Recovery code for 2FA
@ -125,7 +125,7 @@ pub struct RegisterRequest {
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_memory: Option<i32>, // Argon2 memory parameter (15-1024 MB)
pub kdf_parallelism: Option<i32>, // Argon2 parallelism parameter (1-16)
}

View file

@ -5,7 +5,10 @@ use axum::{
use std::sync::Arc;
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 {
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/import", post(import::import_data))
.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}", post(ciphers::update_cipher))
// 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),
)
// WebAuthn (stub - prevents 404 errors, passkeys not supported)
.route(
"/api/webauthn",
get(webauth::get_webauthn_credentials),
)
.route("/api/webauthn", get(webauth::get_webauthn_credentials))
// Two-factor authentication
.route("/api/two-factor", get(twofactor::get_twofactor))
.route(
@ -156,10 +159,7 @@ pub fn api_router(env: Env) -> Router {
"/api/two-factor/disable",
put(twofactor::disable_twofactor_put),
)
.route(
"/api/two-factor/get-recover",
post(twofactor::get_recover),
)
.route("/api/two-factor/get-recover", post(twofactor::get_recover))
.route("/api/two-factor/recover", post(twofactor::recover))
.with_state(app_state)
}