fix(auth): Werkzeug-scrypt-Hashes verifizierbar (Bestandskonten-Login)
Die Flask-Version installierte Werkzeug ungepinnt; Werkzeug >= 3 erzeugt
standardmaessig scrypt-Hashes ('scrypt:32768:8:1$salt$hex') statt
pbkdf2:sha256. Bestandskonten aus dieser Aera (u. a. der Produktiv-Admin)
konnten sich nach dem Rust-Cutover nicht anmelden, weil verify_password nur
pbkdf2 und Argon2 kannte.
- security.rs: verify_werkzeug_scrypt() (Parameter aus dem Hash-String,
dklen aus der Digest-Laenge, Konstantzeit-Vergleich); wie bei pbkdf2 wird
beim ersten erfolgreichen Login transparent auf Argon2id rehasht
- Unit-Test mit gegen hashlib.scrypt reproduziertem Referenzvektor
- Cargo.toml: scrypt 0.11 (RustCrypto, ohne Default-Features)
This commit is contained in:
parent
9b8d603e74
commit
f73239211c
|
|
@ -320,6 +320,16 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common 0.1.7",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmov"
|
name = "cmov"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
|
@ -927,6 +937,15 @@ dependencies = [
|
||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
|
|
@ -958,6 +977,7 @@ dependencies = [
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand 0.8.7",
|
"rand 0.8.7",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"scrypt",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
|
@ -1582,12 +1602,32 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "salsa20"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scrypt"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||||
|
dependencies = [
|
||||||
|
"pbkdf2",
|
||||||
|
"salsa20",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ deadpool-postgres = "0.14"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
pbkdf2 = "0.12"
|
pbkdf2 = "0.12"
|
||||||
|
scrypt = { version = "0.11", default-features = false }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ pub fn verify_password(stored: &str, password: &str) -> (bool, bool) {
|
||||||
if let Some(rest) = stored.strip_prefix("pbkdf2:sha256") {
|
if let Some(rest) = stored.strip_prefix("pbkdf2:sha256") {
|
||||||
return (verify_werkzeug_pbkdf2(rest, password), true);
|
return (verify_werkzeug_pbkdf2(rest, password), true);
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = stored.strip_prefix("scrypt:") {
|
||||||
|
// Werkzeug >= 3 Default: "scrypt:32768:8:1$salt$hex" (dklen=64).
|
||||||
|
// Die Flask-Aera installierte Werkzeug ungepinnt -- Konten ab ~2026-07
|
||||||
|
// haben daher scrypt- statt pbkdf2-Hashes (Hotfix 2026-07-15).
|
||||||
|
return (verify_werkzeug_scrypt(rest, password), true);
|
||||||
|
}
|
||||||
match PasswordHash::new(stored) {
|
match PasswordHash::new(stored) {
|
||||||
Ok(parsed) => (
|
Ok(parsed) => (
|
||||||
Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok(),
|
Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok(),
|
||||||
|
|
@ -70,6 +76,38 @@ fn verify_werkzeug_pbkdf2(rest: &str, password: &str) -> bool {
|
||||||
out.ct_eq(&expected).into()
|
out.ct_eq(&expected).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// rest = "<n>:<r>:<p>$<salt>$<hex-digest>" (Werkzeug-scrypt, dklen = Digest-Laenge).
|
||||||
|
fn verify_werkzeug_scrypt(rest: &str, password: &str) -> bool {
|
||||||
|
let mut parts = rest.splitn(3, '$');
|
||||||
|
let (Some(params), Some(salt), Some(expected_hex)) = (parts.next(), parts.next(), parts.next()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut p = params.splitn(3, ':');
|
||||||
|
let (Some(n), Some(r), Some(pp)) = (
|
||||||
|
p.next().and_then(|v| v.parse::<u64>().ok()),
|
||||||
|
p.next().and_then(|v| v.parse::<u32>().ok()),
|
||||||
|
p.next().and_then(|v| v.parse::<u32>().ok()),
|
||||||
|
) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !n.is_power_of_two() || n < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let log_n = n.trailing_zeros() as u8;
|
||||||
|
let Ok(expected) = hex::decode(expected_hex) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(sparams) = scrypt::Params::new(log_n, r, pp, expected.len()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut out = vec![0u8; expected.len()];
|
||||||
|
if scrypt::scrypt(password.as_bytes(), salt.as_bytes(), &sparams, &mut out).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
|
out.ct_eq(&expected).into()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Passwort-Policy ────────────────────────────────────────────────────────────
|
// ── Passwort-Policy ────────────────────────────────────────────────────────────
|
||||||
/// None wenn ok, sonst deutsche Fehlermeldung. Laenge vor Komplexitaet
|
/// None wenn ok, sonst deutsche Fehlermeldung. Laenge vor Komplexitaet
|
||||||
/// (BSI ORP.4 / NIST SP 800-63B), nur reine Ziffernfolgen werden abgelehnt.
|
/// (BSI ORP.4 / NIST SP 800-63B), nur reine Ziffernfolgen werden abgelehnt.
|
||||||
|
|
@ -169,6 +207,17 @@ mod tests {
|
||||||
assert!(!verify_password(h, "falsch").0);
|
assert!(!verify_password(h, "falsch").0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn werkzeug_scrypt() {
|
||||||
|
// werkzeug.security.generate_password_hash("test-passwort-123") mit Werkzeug>=3
|
||||||
|
// reproduziert mit hashlib.scrypt(b'test-passwort-123', salt=b'abcdefgh', n=32768, r=8, p=1, dklen=64)
|
||||||
|
let h = "scrypt:32768:8:1$abcdefgh$f79ff45b82699a791b5bf9814df79bec4a15061000ab1b1df157bb9d1423b8724b69fffbeac3a44ba206182a85bf11e777beda0d512f48718edd12c74c619b11";
|
||||||
|
let (ok, rehash) = verify_password(h, "test-passwort-123");
|
||||||
|
assert!(rehash);
|
||||||
|
assert!(ok, "werkzeug-scrypt-hash muss verifizierbar sein");
|
||||||
|
assert!(!verify_password(h, "falsch").0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ssrf_guard() {
|
fn ssrf_guard() {
|
||||||
assert!(endpoint_url_problem("").is_none());
|
assert!(endpoint_url_problem("").is_none());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue