Skip to content

Commit cc3ad78

Browse files
committed
feat(auth): configurable Argon2id params with transparent on-login rehash
Introduces `Argon2Config` in config/auth.rs (OWASP 2024+ defaults: m=19456 KiB, t=2, p=1) and wires it through `CredentialStore`, `LockoutPolicy`, and all hash-call sites. `hash_password_argon2` and `verify_argon2` now accept the config instead of using `Argon2::default()`. Replaces `verify_argon2` with `verify_argon2_with_rehash`, which returns a `VerifyOutcome` that includes an optional new PHC string when the stored hash is weaker than the current config. On successful login, `CredentialStore::verify` writes the stronger hash back under a write lock; failure is non-fatal (logged as a warning). The no-downgrade rule is enforced: if the stored hash is equal or stronger, no rehash is performed. Also adds `zeroize` to the workspace and as a dependency in nodedb, nodedb-wal, and nodedb-types, and reformats a multi-argument assert in the spatial integration test to comply with rustfmt.
1 parent b546935 commit cc3ad78

11 files changed

Lines changed: 424 additions & 31 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ base64 = "0.22"
8484
getrandom = { version = "0.3", features = ["std"] }
8585
aes-gcm = "0.10"
8686
argon2 = "0.5"
87+
zeroize = { version = "1", features = ["derive"] }
8788
aws-sdk-kms = { version = "1", default-features = false, features = ["rustls"] }
8889
aws-config = { version = "1", default-features = false, features = ["rustls", "rt-tokio"] }
8990

nodedb/src/config/auth.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
11
use serde::{Deserialize, Serialize};
22

3+
// ── OWASP Argon2id 2024+ minimum recommended parameters ──────────────────────
4+
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
5+
// m=19456 KiB / t=2 / p=1 is the stated OWASP minimum for Argon2id.
6+
// NodeDB ships with those minimums as defaults. Operators may increase them;
7+
// decreasing below these values is their responsibility.
8+
9+
fn default_argon2_memory_kib() -> u32 {
10+
19_456
11+
}
12+
fn default_argon2_time_cost() -> u32 {
13+
2
14+
}
15+
fn default_argon2_parallelism() -> u32 {
16+
1
17+
}
18+
fn default_argon2_output_len() -> usize {
19+
32
20+
}
21+
22+
/// Argon2id hashing parameters.
23+
///
24+
/// Defaults follow OWASP Argon2id 2024+ guidance (m=19456 KiB / t=2 / p=1).
25+
///
26+
/// **Upgrade rule**: on successful login, the stored hash is transparently
27+
/// rehashed if *any* stored parameter is strictly weaker than the configured
28+
/// one. If the stored hash is *stronger* (operator tuned the dial down), the
29+
/// hash is left unchanged — no silent downgrade.
30+
///
31+
/// **Existing config files**: all fields have serde defaults so existing files
32+
/// that omit `[auth.argon2]` continue to load and use the OWASP defaults.
33+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34+
pub struct Argon2Config {
35+
/// Memory cost in KiB. OWASP minimum: 19456 (19 MiB).
36+
#[serde(default = "default_argon2_memory_kib")]
37+
pub memory_kib: u32,
38+
/// Number of iterations (time cost). OWASP minimum: 2.
39+
#[serde(default = "default_argon2_time_cost")]
40+
pub time_cost: u32,
41+
/// Degree of parallelism (lanes). OWASP minimum: 1.
42+
#[serde(default = "default_argon2_parallelism")]
43+
pub parallelism: u32,
44+
/// Output length in bytes. 32 bytes = 256-bit key material.
45+
#[serde(default = "default_argon2_output_len")]
46+
pub output_len: usize,
47+
}
48+
49+
impl Default for Argon2Config {
50+
fn default() -> Self {
51+
Self {
52+
memory_kib: default_argon2_memory_kib(),
53+
time_cost: default_argon2_time_cost(),
54+
parallelism: default_argon2_parallelism(),
55+
output_len: default_argon2_output_len(),
56+
}
57+
}
58+
}
59+
360
/// Authentication mode.
461
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562
#[serde(rename_all = "lowercase")]
@@ -275,6 +332,11 @@ pub struct AuthConfig {
275332
#[serde(default)]
276333
pub audit_max_entries: u64,
277334

335+
/// Argon2id hashing parameters used for new hashes and rehash decisions.
336+
/// Existing config files that omit this section use the OWASP defaults.
337+
#[serde(default)]
338+
pub argon2: Argon2Config,
339+
278340
/// JWT authentication configuration (JWKS providers, algorithms, etc.).
279341
/// If not present, JWT auth is disabled.
280342
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -398,6 +460,7 @@ impl Default for AuthConfig {
398460
password_expiry_grace_days: 0,
399461
audit_retention_days: 0,
400462
audit_max_entries: 0,
463+
argon2: Argon2Config::default(),
401464
jwt: None,
402465
rate_limit: None,
403466
metering: None,

nodedb/src/control/security/audit/entry.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,7 @@ mod tests {
117117
let h = hash_entry(&entry);
118118
// Pinned hash. Updating this string requires explicit reasoning — encoding changes break audit chain compatibility.
119119
assert_eq!(
120-
h,
121-
"0394994e7dda6e6ea99b47a9d6a73b305056ed8d34d5573286b2e42b158a7985",
120+
h, "0394994e7dda6e6ea99b47a9d6a73b305056ed8d34d5573286b2e42b158a7985",
122121
"canonical hash changed — audit chain encoding must not drift"
123122
);
124123
}
@@ -134,8 +133,7 @@ mod tests {
134133
let h = hash_entry(&entry);
135134
// Pinned hash. Updating this string requires explicit reasoning — encoding changes break audit chain compatibility.
136135
assert_eq!(
137-
h,
138-
"a7eac81c35ed115b7345ad6ac9e1ccbfb78d46c5af9b4647ebe6d2560d4a00c2",
136+
h, "a7eac81c35ed115b7345ad6ac9e1ccbfb78d46c5af9b4647ebe6d2560d4a00c2",
139137
"canonical hash for AuditCheckpoint (discriminant 25) must not drift"
140138
);
141139
}

0 commit comments

Comments
 (0)