diff --git a/src/shadow-core/src/crypt.rs b/src/shadow-core/src/crypt.rs index f768d07..8e1413f 100644 --- a/src/shadow-core/src/crypt.rs +++ b/src/shadow-core/src/crypt.rs @@ -9,7 +9,6 @@ //! is permitted, because `crypt(3)` is a C library function. use std::ffi::CString; -use std::io::Read; use subtle::ConstantTimeEq; @@ -24,7 +23,7 @@ unsafe extern "C" { const SALT_CHARS: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /// Supported crypt(3) hash methods. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CryptMethod { /// SHA-256 ($5$) Sha256, @@ -48,9 +47,13 @@ impl CryptMethod { /// Generate a random salt string for crypt(3). fn generate_salt(method: CryptMethod, rounds: Option) -> Result { let mut rand_bytes = [0u8; 16]; - std::fs::File::open("/dev/urandom") - .and_then(|mut f| f.read_exact(&mut rand_bytes)) - .map_err(|e| ShadowError::Other(format!("cannot read /dev/urandom: {e}").into()))?; + + // Use getrandom(2) syscall — works in chroot environments without /dev/urandom. + // SAFETY: getrandom(2) writes into a valid buffer and returns bytes written or -1. + let ret = unsafe { libc::getrandom(rand_bytes.as_mut_ptr().cast(), rand_bytes.len(), 0) }; + if ret < 0 || ret.cast_unsigned() < rand_bytes.len() { + return Err(ShadowError::Other("getrandom(2) failed".into())); + } let salt_str: String = rand_bytes .iter() @@ -58,9 +61,14 @@ fn generate_salt(method: CryptMethod, rounds: Option) -> Result Ok(format!("{prefix}rounds={r}${salt_str}$")), - None => Ok(format!("{prefix}{salt_str}$")), + match (method, rounds) { + (CryptMethod::Sha256 | CryptMethod::Sha512, Some(r)) => { + Ok(format!("{prefix}rounds={r}${salt_str}$")) + } + (CryptMethod::Yescrypt, Some(_)) => Err(ShadowError::Auth( + "rounds parameter is not supported for yescrypt".into(), + )), + (_, None) => Ok(format!("{prefix}{salt_str}$")), } } @@ -98,6 +106,13 @@ pub fn hash_password( .to_str() .map_err(|_| ShadowError::Auth("crypt(3) returned invalid UTF-8".into()))?; + // crypt(3) returns "*0" or "*1" when the method is unsupported. + if hash.starts_with('*') { + return Err(ShadowError::Auth( + format!("crypt(3) does not support {method:?} on this system").into(), + )); + } + Ok(hash.to_string()) } @@ -131,3 +146,84 @@ pub fn verify_password(password: &str, hash: &str) -> Result // that could leak password hash information. Ok(result_str.as_bytes().ct_eq(hash.as_bytes()).into()) } + +#[cfg(test)] +mod tests { + use super::*; + + // crypt(3) uses a process-wide static buffer — serialize all tests + // that call it to avoid SIGSEGV from concurrent access. + static CRYPT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[test] + fn test_hash_verify_sha512() { + let _guard = CRYPT_LOCK.lock().expect("lock"); + let hash = hash_password("secret", CryptMethod::Sha512, None) + .expect("hash_password should succeed"); + assert!( + hash.starts_with("$6$"), + "SHA-512 hash should start with $6$" + ); + assert!( + verify_password("secret", &hash).expect("verify should succeed"), + "correct password should verify" + ); + assert!( + !verify_password("wrong", &hash).expect("verify should succeed"), + "wrong password should not verify" + ); + } + + #[test] + fn test_hash_verify_sha256() { + let _guard = CRYPT_LOCK.lock().expect("lock"); + let hash = hash_password("secret", CryptMethod::Sha256, None) + .expect("hash_password should succeed"); + assert!( + hash.starts_with("$5$"), + "SHA-256 hash should start with $5$" + ); + assert!(verify_password("secret", &hash).expect("verify should succeed")); + } + + #[test] + fn test_hash_verify_yescrypt() { + let _guard = CRYPT_LOCK.lock().expect("lock"); + // musl libc doesn't support yescrypt — skip gracefully. + let Ok(hash) = hash_password("secret", CryptMethod::Yescrypt, None) else { + return; + }; + assert!( + hash.starts_with("$y$"), + "yescrypt hash should start with $y$" + ); + assert!(verify_password("secret", &hash).expect("verify should succeed")); + } + + #[test] + fn test_sha_rounds_applied() { + let _guard = CRYPT_LOCK.lock().expect("lock"); + // musl libc doesn't support SHA rounds — skip gracefully. + let Ok(hash) = hash_password("secret", CryptMethod::Sha512, Some(10000)) else { + return; + }; + assert!( + hash.starts_with("$6$rounds=10000$"), + "rounds should appear in hash" + ); + assert!(verify_password("secret", &hash).expect("verify should succeed")); + } + + #[test] + fn test_yescrypt_rejects_rounds() { + let result = hash_password("secret", CryptMethod::Yescrypt, Some(10000)); + assert!(result.is_err(), "yescrypt should reject rounds parameter"); + } + + #[test] + fn test_generate_salt_unique() { + let s1 = generate_salt(CryptMethod::Sha512, None).expect("salt gen"); + let s2 = generate_salt(CryptMethod::Sha512, None).expect("salt gen"); + assert_ne!(s1, s2, "two salts should differ"); + } +} diff --git a/src/shadow-core/src/shadow.rs b/src/shadow-core/src/shadow.rs index 5991d23..96eb1f5 100644 --- a/src/shadow-core/src/shadow.rs +++ b/src/shadow-core/src/shadow.rs @@ -114,7 +114,7 @@ impl ShadowEntry { pub fn days_since_epoch() -> Result { let secs = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .map_err(|_| ShadowError::Parse("system clock is before Unix epoch".into()))?; + .map_err(|_| ShadowError::Other("system clock is before Unix epoch".into()))?; let secs = i64::try_from(secs.as_secs()).unwrap_or(i64::MAX); Ok(secs / 86400) } diff --git a/src/uu/chpasswd/src/chpasswd.rs b/src/uu/chpasswd/src/chpasswd.rs index 3d24b57..be551b9 100644 --- a/src/uu/chpasswd/src/chpasswd.rs +++ b/src/uu/chpasswd/src/chpasswd.rs @@ -231,15 +231,49 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let is_encrypted = matches.get_flag(options::ENCRYPTED); let use_md5 = matches.get_flag(options::MD5); let crypt_method = matches.get_one::(options::CRYPT_METHOD); - let sha_rounds = matches.get_one::(options::SHA_ROUNDS); + + // Reject -m unconditionally — MD5 is insecure. + if use_md5 { + return Err(ChpasswdError::UnexpectedFailure( + "MD5 is insecure and not supported; use -c SHA512 instead".into(), + ) + .into()); + } + + // Validate --sha-rounds range. + let sha_rounds = match matches.get_one::(options::SHA_ROUNDS).copied() { + Some(r @ 1..=i64::MAX) => match u32::try_from(r) { + Ok(v) => Some(v), + Err(_) => { + return Err(ChpasswdError::UnexpectedFailure(format!( + "invalid value for --sha-rounds '{r}': must be between 1 and {}", + u32::MAX + )) + .into()); + } + }, + Some(r) => { + return Err(ChpasswdError::UnexpectedFailure(format!( + "invalid value for --sha-rounds '{r}': must be between 1 and {}", + u32::MAX + )) + .into()); + } + None => None, + }; // Determine the hashing method for plaintext mode. let hash_config = if is_encrypted { None } else { - let method = resolve_crypt_method(crypt_method.map(String::as_str), use_md5)?; - let rounds = sha_rounds.and_then(|&r| u32::try_from(r).ok()); - Some((method, rounds)) + let method = resolve_crypt_method(crypt_method.map(String::as_str))?; + if sha_rounds.is_some() && method == shadow_core::crypt::CryptMethod::Yescrypt { + return Err(ChpasswdError::UnexpectedFailure( + "--sha-rounds is not supported with YESCRYPT".into(), + ) + .into()); + } + Some((method, sha_rounds)) }; // Read all pairs from stdin before acquiring locks. @@ -395,16 +429,15 @@ fn apply_password_changes( // Helpers // --------------------------------------------------------------------------- -/// Map `-c` / `-m` flags to a `CryptMethod`. +/// Map `-c` flag to a `CryptMethod`. fn resolve_crypt_method( method: Option<&str>, - use_md5: bool, ) -> Result { use shadow_core::crypt::CryptMethod; match method { Some("SHA256") => Ok(CryptMethod::Sha256), - Some("SHA512") => Ok(CryptMethod::Sha512), + Some("SHA512") | None => Ok(CryptMethod::Sha512), Some("YESCRYPT") => Ok(CryptMethod::Yescrypt), Some("MD5" | "DES") => Err(ChpasswdError::UnexpectedFailure( "MD5 and DES are insecure and not supported for plaintext hashing".into(), @@ -412,10 +445,6 @@ fn resolve_crypt_method( Some(other) => Err(ChpasswdError::UnexpectedFailure(format!( "unknown crypt method: {other}" ))), - None if use_md5 => Err(ChpasswdError::UnexpectedFailure( - "MD5 is insecure and not supported; use -c SHA512 instead".into(), - )), - None => Ok(CryptMethod::Sha512), } }