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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "cmov"
|
||||
version = "0.5.4"
|
||||
|
|
@ -927,6 +937,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
|
|
@ -958,6 +977,7 @@ dependencies = [
|
|||
"pbkdf2",
|
||||
"rand 0.8.7",
|
||||
"reqwest",
|
||||
"scrypt",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
|
@ -1582,12 +1602,32 @@ version = "1.0.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ deadpool-postgres = "0.14"
|
|||
chrono = { version = "0.4", features = ["serde"] }
|
||||
argon2 = "0.5"
|
||||
pbkdf2 = "0.12"
|
||||
scrypt = { version = "0.11", default-features = false }
|
||||
sha2 = "0.10"
|
||||
subtle = "2"
|
||||
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") {
|
||||
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) {
|
||||
Ok(parsed) => (
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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 ────────────────────────────────────────────────────────────
|
||||
/// None wenn ok, sonst deutsche Fehlermeldung. Laenge vor Komplexitaet
|
||||
/// (BSI ORP.4 / NIST SP 800-63B), nur reine Ziffernfolgen werden abgelehnt.
|
||||
|
|
@ -169,6 +207,17 @@ mod tests {
|
|||
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]
|
||||
fn ssrf_guard() {
|
||||
assert!(endpoint_url_problem("").is_none());
|
||||
|
|
|
|||
Loading…
Reference in New Issue