chore: format code
This commit is contained in:
parent
3ba2f09ea3
commit
38b978ca6e
14 changed files with 219 additions and 171 deletions
16
src/auth.rs
16
src/auth.rs
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue