diff --git a/.env.template b/.env.template index 03990820..b62a6af2 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 ba36fc9b..b8e99840 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1236,7 +1236,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 6ff09467..247db0c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -645,6 +645,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 @@ -946,6 +958,26 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> 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 ebc72101..7b9bc067 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,