Browse Source

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) <noreply@anthropic.com>
pull/6983/head
Don Kendall 3 weeks ago
parent
commit
2d68adcfed
  1. 14
      .env.template
  2. 2
      src/api/core/accounts.rs
  3. 32
      src/config.rs
  4. 40
      src/db/models/user.rs

14
.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

2
src/api/core/accounts.rs

@ -1232,7 +1232,7 @@ pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
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!({

32
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}.",));

40
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<i32> {
if CONFIG.client_kdf_type() == UserKdfType::Argon2id as i32 {
Some(CONFIG.client_kdf_memory())
} else {
None
}
}
pub fn client_kdf_parallelism_default() -> Option<i32> {
if CONFIG.client_kdf_type() == UserKdfType::Argon2id as i32 {
Some(CONFIG.client_kdf_parallelism())
} else {
None
}
}
pub fn new(email: &str, name: Option<String>) -> 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,

Loading…
Cancel
Save