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
|
/// 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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -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?
|
||||||
|
|
|
||||||
|
|
@ -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!({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue