mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
chpasswd: implement plaintext password hashing
Add hash_password() and CryptMethod to shadow-core::crypt alongside the existing verify_password(). Supports SHA-256, SHA-512, and yescrypt; rejects MD5 and DES as insecure. chpasswd now accepts plaintext mode (the default) and hashes passwords via crypt(3) before writing. Previously only -e (pre-encrypted) worked. Fixes #128
This commit is contained in:
@@ -3,12 +3,13 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Safe wrapper around POSIX `crypt(3)` for password hash verification.
|
||||
//! Safe wrapper around POSIX `crypt(3)` for password hashing and verification.
|
||||
//!
|
||||
//! This is one of only two modules (along with `pam`) where `unsafe_code`
|
||||
//! is permitted, because `crypt(3)` is a C library function.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::io::Read;
|
||||
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
@@ -19,6 +20,87 @@ unsafe extern "C" {
|
||||
fn crypt(key: *const libc::c_char, salt: *const libc::c_char) -> *mut libc::c_char;
|
||||
}
|
||||
|
||||
/// crypt(3) salt alphabet (POSIX: [a-zA-Z0-9./]).
|
||||
const SALT_CHARS: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/// Supported crypt(3) hash methods.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CryptMethod {
|
||||
/// SHA-256 ($5$)
|
||||
Sha256,
|
||||
/// SHA-512 ($6$) — recommended default
|
||||
Sha512,
|
||||
/// yescrypt ($y$)
|
||||
Yescrypt,
|
||||
}
|
||||
|
||||
impl CryptMethod {
|
||||
/// The crypt(3) prefix for this method.
|
||||
fn prefix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Sha256 => "$5$",
|
||||
Self::Sha512 => "$6$",
|
||||
Self::Yescrypt => "$y$j9T$",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random salt string for crypt(3).
|
||||
fn generate_salt(method: CryptMethod, rounds: Option<u32>) -> Result<String, ShadowError> {
|
||||
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()))?;
|
||||
|
||||
let salt_str: String = rand_bytes
|
||||
.iter()
|
||||
.map(|&b| SALT_CHARS[(b as usize) % SALT_CHARS.len()] as char)
|
||||
.collect();
|
||||
|
||||
let prefix = method.prefix();
|
||||
match rounds {
|
||||
Some(r) => Ok(format!("{prefix}rounds={r}${salt_str}$")),
|
||||
None => Ok(format!("{prefix}{salt_str}$")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash a plaintext password using crypt(3).
|
||||
///
|
||||
/// Returns the full crypt(3) hash string (e.g. `$6$salt$hash...`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ShadowError` if the password contains null bytes, the salt
|
||||
/// cannot be generated, or crypt(3) fails.
|
||||
pub fn hash_password(
|
||||
password: &str,
|
||||
method: CryptMethod,
|
||||
rounds: Option<u32>,
|
||||
) -> Result<String, ShadowError> {
|
||||
let salt = generate_salt(method, rounds)?;
|
||||
let c_password = CString::new(password)
|
||||
.map_err(|_| ShadowError::Auth("password contains null byte".into()))?;
|
||||
let c_salt = CString::new(salt.as_str())
|
||||
.map_err(|_| ShadowError::Auth("salt contains null byte".into()))?;
|
||||
|
||||
// SAFETY: crypt() is provided by libcrypt/glibc. Both arguments are valid
|
||||
// null-terminated C strings. The returned pointer is to a static/thread-local
|
||||
// buffer managed by crypt().
|
||||
let result = unsafe { crypt(c_password.as_ptr(), c_salt.as_ptr()) };
|
||||
|
||||
if result.is_null() {
|
||||
return Err(ShadowError::Auth("crypt(3) returned NULL".into()));
|
||||
}
|
||||
|
||||
// SAFETY: crypt() returned a non-null pointer to a null-terminated string.
|
||||
let result_str = unsafe { std::ffi::CStr::from_ptr(result) };
|
||||
let hash = result_str
|
||||
.to_str()
|
||||
.map_err(|_| ShadowError::Auth("crypt(3) returned invalid UTF-8".into()))?;
|
||||
|
||||
Ok(hash.to_string())
|
||||
}
|
||||
|
||||
/// Verify a plaintext password against a crypt(3) hash.
|
||||
///
|
||||
/// Returns `true` if the password matches the hash, `false` otherwise.
|
||||
|
||||
Reference in New Issue
Block a user