From 2d68adcfed42d66548fb26c9d69e5b6573dbb2f1 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Sat, 21 Mar 2026 15:24:25 -0400 Subject: [PATCH] feat: add configurable default KDF type for new user registrations Add CLIENT_KDF_TYPE, CLIENT_KDF_ITERATIONS, CLIENT_KDF_MEMORY, and CLIENT_KDF_PARALLELISM environment variables to allow server admins to set the default KDF for new user registrations. Currently the default KDF is hardcoded to PBKDF2 with 600,000 iterations. Argon2id is memory-hard and significantly more resistant to GPU-based brute-force attacks, but admins have no way to set it as the default without modifying source code. Existing users are unaffected and can change their KDF in account settings. Setting CLIENT_KDF_TYPE=1 enables Argon2id with sensible defaults (3 iterations, 64MB memory, 4 parallelism). Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.template | 14 ++++++++++++++ src/api/core/accounts.rs | 2 +- src/config.rs | 32 ++++++++++++++++++++++++++++++++ src/db/models/user.rs | 40 ++++++++++++++++++++++++++++++++++------ 4 files changed, 81 insertions(+), 7 deletions(-) diff --git a/.env.template b/.env.template index c5563a1d..411c4af7 100644 --- a/.env.template +++ b/.env.template @@ -306,6 +306,20 @@ ## The default for new users. If changed, it will be updated during login for existing users. # PASSWORD_ITERATIONS=600000 +## Default KDF type for new user registrations. 0 = PBKDF2, 1 = Argon2id. +## Argon2id is recommended as it is memory-hard and more resistant to GPU-based brute-force attacks. +## When set to 1, CLIENT_KDF_ITERATIONS defaults to 3, CLIENT_KDF_MEMORY to 64, CLIENT_KDF_PARALLELISM to 4. +## Existing users are not affected; they can change their KDF in account settings. +# CLIENT_KDF_TYPE=0 +## Default KDF iterations for new user registrations. +## For PBKDF2 (type 0): minimum 100000, default 600000. +## For Argon2id (type 1): minimum 1, default 3. +# CLIENT_KDF_ITERATIONS=600000 +## Default Argon2id memory parameter (in MB) for new user registrations. Only used when CLIENT_KDF_TYPE=1. +# CLIENT_KDF_MEMORY=64 +## Default Argon2id parallelism parameter for new user registrations. Only used when CLIENT_KDF_TYPE=1. +# CLIENT_KDF_PARALLELISM=4 + ## Controls whether users can set or show password hints. This setting applies globally to all users. # PASSWORD_HINTS_ALLOWED=true diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index d91eb4cd..e1642c5e 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1232,7 +1232,7 @@ pub async fn _prelogin(data: Json, conn: DbConn) -> Json { let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await { Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism), - None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None), + None => (User::client_kdf_type_default(), User::client_kdf_iter_default(), User::client_kdf_memory_default(), User::client_kdf_parallelism_default()), }; Json(json!({ diff --git a/src/config.rs b/src/config.rs index 0221fd9a..74976d77 100644 --- a/src/config.rs +++ b/src/config.rs @@ -642,6 +642,18 @@ make_config! { /// Password iterations |> Number of server-side passwords hashing iterations for the password hash. /// The default for new users. If changed, it will be updated during login for existing users. password_iterations: i32, true, def, 600_000; + /// Client KDF type |> The default KDF type for new user registrations. 0 = PBKDF2, 1 = Argon2id. + /// Argon2id is recommended as it is memory-hard and resistant to GPU-based attacks. + client_kdf_type: i32, true, def, 0; + /// Client KDF iterations |> The default KDF iterations for new user registrations. + /// For PBKDF2: default 600000. For Argon2id: default 3. + client_kdf_iterations: i32, true, def, 600_000; + /// Client KDF memory (MB) |> The default Argon2id memory parameter (in MB) for new user registrations. + /// Only used when client_kdf_type = 1 (Argon2id). Default: 64. + client_kdf_memory: i32, true, def, 64; + /// Client KDF parallelism |> The default Argon2id parallelism parameter for new user registrations. + /// Only used when client_kdf_type = 1 (Argon2id). Default: 4. + client_kdf_parallelism: i32, true, def, 4; /// Allow password hints |> Controls whether users can set or show password hints. This setting applies globally to all users. password_hints_allowed: bool, true, def, true; /// Show password hint (Know the risks!) |> Controls whether a password hint should be shown directly in the web page @@ -943,6 +955,26 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { err!("PASSWORD_ITERATIONS should be at least 100000 or higher. The default is 600000!"); } + if cfg.client_kdf_type < 0 || cfg.client_kdf_type > 1 { + err!("CLIENT_KDF_TYPE must be 0 (PBKDF2) or 1 (Argon2id)."); + } + + if cfg.client_kdf_type == 0 && cfg.client_kdf_iterations < 100_000 { + err!("CLIENT_KDF_ITERATIONS must be at least 100000 for PBKDF2."); + } + + if cfg.client_kdf_type == 1 { + if cfg.client_kdf_iterations < 1 { + err!("CLIENT_KDF_ITERATIONS must be at least 1 for Argon2id."); + } + if cfg.client_kdf_memory < 15 || cfg.client_kdf_memory > 1024 { + err!("CLIENT_KDF_MEMORY must be between 15 and 1024 (MB) for Argon2id."); + } + if cfg.client_kdf_parallelism < 1 || cfg.client_kdf_parallelism > 16 { + err!("CLIENT_KDF_PARALLELISM must be between 1 and 16 for Argon2id."); + } + } + let limit = 256; if cfg.database_max_conns < 1 || cfg.database_max_conns > limit { err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",)); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index e88c7296..f63ff22a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -102,8 +102,36 @@ pub struct UserStampException { /// Local methods impl User { - pub const CLIENT_KDF_TYPE_DEFAULT: i32 = UserKdfType::Pbkdf2 as i32; - pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000; + pub fn client_kdf_type_default() -> i32 { + CONFIG.client_kdf_type() + } + + pub fn client_kdf_iter_default() -> i32 { + let kdf_type = CONFIG.client_kdf_type(); + let configured = CONFIG.client_kdf_iterations(); + // If the admin set Argon2id but left iterations at the PBKDF2 default, use sensible Argon2id default + if kdf_type == UserKdfType::Argon2id as i32 && configured >= 100_000 { + 3 + } else { + configured + } + } + + pub fn client_kdf_memory_default() -> Option { + if CONFIG.client_kdf_type() == UserKdfType::Argon2id as i32 { + Some(CONFIG.client_kdf_memory()) + } else { + None + } + } + + pub fn client_kdf_parallelism_default() -> Option { + if CONFIG.client_kdf_type() == UserKdfType::Argon2id as i32 { + Some(CONFIG.client_kdf_parallelism()) + } else { + None + } + } pub fn new(email: &str, name: Option) -> Self { let now = Utc::now().naive_utc(); @@ -140,10 +168,10 @@ impl User { equivalent_domains: "[]".to_string(), excluded_globals: "[]".to_string(), - client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT, - client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT, - client_kdf_memory: None, - client_kdf_parallelism: None, + client_kdf_type: Self::client_kdf_type_default(), + client_kdf_iter: Self::client_kdf_iter_default(), + client_kdf_memory: Self::client_kdf_memory_default(), + client_kdf_parallelism: Self::client_kdf_parallelism_default(), api_key: None,