forked from proxmox-mirrors/proxmox

These are statically sized arrays, not slices, they cannot be empty. Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
205 lines
7.3 KiB
Rust
205 lines
7.3 KiB
Rust
//! Rust bindings for libcrypt
|
||
//!
|
||
//! this may fail if we ever pull in dependencies that also link with libcrypt. we may eventually
|
||
//! want to switch to pure rust re-implementations of libcrypt.
|
||
|
||
use std::ffi::{CStr, CString};
|
||
|
||
use anyhow::{bail, Error};
|
||
|
||
// from libcrypt1, 'lib/crypt.h.in'
|
||
const CRYPT_OUTPUT_SIZE: usize = 384;
|
||
const CRYPT_MAX_PASSPHRASE_SIZE: usize = 512;
|
||
const CRYPT_DATA_RESERVED_SIZE: usize = 767;
|
||
const CRYPT_DATA_INTERNAL_SIZE: usize = 30720;
|
||
const CRYPT_GENSALT_OUTPUT_SIZE: usize = 192;
|
||
|
||
// the hash prefix selects the password hashing method, currently this is yescrypt. check `man
|
||
// crypt(5)` for more info
|
||
pub const HASH_PREFIX: &str = "$y$";
|
||
|
||
// the cpu cost of the password hashing function. depends on the hashing function, see`man
|
||
// crypt_gensalt(3)` and `man crypt(5) for more info
|
||
//
|
||
// `5` selects a good medium cpu time hardness that seems to be widely used by e.g. Debian
|
||
// see `YESCRYPT_COST_FACTOR` in `/etc/login.defs`
|
||
const HASH_COST: u64 = 5;
|
||
|
||
#[repr(C)]
|
||
struct CryptData {
|
||
output: [libc::c_char; CRYPT_OUTPUT_SIZE],
|
||
setting: [libc::c_char; CRYPT_OUTPUT_SIZE],
|
||
input: [libc::c_char; CRYPT_MAX_PASSPHRASE_SIZE],
|
||
reserved: [libc::c_char; CRYPT_DATA_RESERVED_SIZE],
|
||
initialized: libc::c_char,
|
||
internal: [libc::c_char; CRYPT_DATA_INTERNAL_SIZE],
|
||
}
|
||
|
||
/// Encrypt a password - see man crypt(3)
|
||
pub fn crypt(password: &[u8], salt: &[u8]) -> Result<String, Error> {
|
||
#[link(name = "crypt")]
|
||
extern "C" {
|
||
#[link_name = "crypt_r"]
|
||
fn __crypt_r(
|
||
key: *const libc::c_char,
|
||
salt: *const libc::c_char,
|
||
data: *mut CryptData,
|
||
) -> *mut libc::c_char;
|
||
}
|
||
|
||
let mut data: CryptData = unsafe { std::mem::zeroed() };
|
||
for (i, c) in salt.iter().take(data.setting.len() - 1).enumerate() {
|
||
data.setting[i] = *c as libc::c_char;
|
||
}
|
||
for (i, c) in password.iter().take(data.input.len() - 1).enumerate() {
|
||
data.input[i] = *c as libc::c_char;
|
||
}
|
||
|
||
let res = unsafe {
|
||
let status = __crypt_r(
|
||
&data.input as *const _,
|
||
&data.setting as *const _,
|
||
&mut data as *mut _,
|
||
);
|
||
if status.is_null() {
|
||
bail!("internal error: crypt_r returned null pointer");
|
||
}
|
||
|
||
// according to man crypt(3):
|
||
//
|
||
// > Upon error, crypt_r, crypt_rn, and crypt_ra write an invalid hashed passphrase to the
|
||
// > output field of their data argument, and crypt writes an invalid hash to its static
|
||
// > storage area. This string will be shorter than 13 characters, will begin with a ‘*’,
|
||
// > and will not compare equal to setting.
|
||
if data.output[0] == '*' as libc::c_char {
|
||
bail!("internal error: crypt_r returned invalid hash");
|
||
}
|
||
CStr::from_ptr(&data.output as *const _)
|
||
};
|
||
Ok(String::from(res.to_str()?))
|
||
}
|
||
|
||
/// Rust wrapper around `crypt_gensalt_rn` from libcrypt. Useful to generate salts for crypt.
|
||
///
|
||
/// - `prefix`: The prefix that selects the hashing method to use (see `man crypt(5)`)
|
||
/// - `count`: The CPU time cost parameter (e.g., for `yescrypt` between 1 and 11, see `man
|
||
/// crypt(5)`)
|
||
/// - `rbytes`: The byte slice that contains cryptographically random bytes for generating the salt
|
||
pub fn crypt_gensalt(prefix: &str, count: u64, rbytes: &[u8]) -> Result<String, Error> {
|
||
#[link(name = "crypt")]
|
||
extern "C" {
|
||
#[link_name = "crypt_gensalt_rn"]
|
||
fn __crypt_gensalt_rn(
|
||
prefix: *const libc::c_char,
|
||
count: libc::c_ulong,
|
||
// `crypt_gensalt_rn`'s signature expects a char pointer here, which would be a pointer
|
||
// to an `i8` slice in rust. however, this is interpreted as raw bytes that are used as
|
||
// entropy, which in rust usually is a `u8` slice. so use this signature to avoid a
|
||
// pointless transmutation (random bytes are random, whether interpreted as `i8` or
|
||
// `u8`)
|
||
rbytes: *const u8,
|
||
nrbytes: libc::c_int,
|
||
output: *mut libc::c_char,
|
||
output_size: libc::c_int,
|
||
) -> *mut libc::c_char;
|
||
}
|
||
|
||
let prefix = CString::new(prefix)?;
|
||
|
||
#[allow(clippy::useless_conversion)]
|
||
let mut output = [libc::c_char::from(0); CRYPT_GENSALT_OUTPUT_SIZE];
|
||
|
||
let status = unsafe {
|
||
__crypt_gensalt_rn(
|
||
prefix.as_ptr(),
|
||
count,
|
||
rbytes.as_ptr(),
|
||
rbytes.len().try_into()?,
|
||
output.as_mut_ptr(),
|
||
output.len().try_into()?,
|
||
)
|
||
};
|
||
|
||
if status.is_null() {
|
||
bail!("internal error: crypt_gensalt_rn returned a null pointer");
|
||
}
|
||
|
||
// according to man crypt_gensalt_rn(3):
|
||
//
|
||
// > Upon error, in addition to returning a null pointer, crypt_gensalt and crypt_gensalt_rn
|
||
// > will write an invalid setting string to their output buffer, if there is enough space;
|
||
// > this string will begin with a ‘*’ and will not be equal to prefix.
|
||
//
|
||
// while it states that this is "in addition" to returning a null pointer, this isn't how
|
||
// `crypt_r` seems to behave (sometimes only setting an invalid hash) so add this here too just
|
||
// in case.
|
||
if output[0] == '*' as libc::c_char {
|
||
bail!("internal error: crypt_gensalt_rn could not create a valid salt");
|
||
}
|
||
|
||
let res = unsafe { CStr::from_ptr(output.as_ptr()) };
|
||
|
||
Ok(res.to_str()?.to_string())
|
||
}
|
||
|
||
/// Encrypt a password using sha256 hashing method
|
||
pub fn encrypt_pw(password: &str) -> Result<String, Error> {
|
||
// 8*32 = 256 bits security (128+ recommended, see `man crypt(5)`)
|
||
let salt = crate::linux::random_data(32)?;
|
||
|
||
let salt = crypt_gensalt(HASH_PREFIX, HASH_COST, &salt)?;
|
||
|
||
crypt(password.as_bytes(), salt.as_bytes())
|
||
}
|
||
|
||
/// Verify if an encrypted password matches
|
||
pub fn verify_crypt_pw(password: &str, enc_password: &str) -> Result<(), Error> {
|
||
let verify = crypt(password.as_bytes(), enc_password.as_bytes())?;
|
||
|
||
// `openssl::memcmp::eq()`'s runtime does not depend on the content of the arrays only the
|
||
// length, this makes it harder to exploit timing side-channels.
|
||
if verify.len() != enc_password.len()
|
||
|| !openssl::memcmp::eq(verify.as_bytes(), enc_password.as_bytes())
|
||
{
|
||
bail!("invalid credentials");
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[test]
|
||
fn test_hash_and_verify_passphrase() {
|
||
let phrase = "supersecretpassphrasenoonewillguess";
|
||
|
||
let hash = encrypt_pw(phrase).expect("could not hash test password");
|
||
verify_crypt_pw(phrase, &hash).expect("could not verify test password");
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic]
|
||
fn test_wrong_passphrase_fails() {
|
||
let phrase = "supersecretpassphrasenoonewillguess";
|
||
|
||
let hash = encrypt_pw(phrase).expect("could not hash test password");
|
||
verify_crypt_pw("nope", &hash).expect("could not verify test password");
|
||
}
|
||
|
||
#[test]
|
||
fn test_old_passphrase_hash() {
|
||
let phrase = "supersecretpassphrasenoonewillguess";
|
||
// `$5$` -> sha256crypt, our previous default implementation
|
||
let hash = "$5$bx7fjhlS8yMPM3Nc$yRgB5vyoTWeRcYn31RFTg5hAGyTInUq.l0HqLKzRuRC";
|
||
|
||
verify_crypt_pw(phrase, hash).expect("could not verify test password");
|
||
}
|
||
|
||
#[test]
|
||
#[should_panic]
|
||
fn test_old_hash_wrong_passphrase_fails() {
|
||
let phrase = "nope";
|
||
// `$5$` -> sha256crypt, our previous default implementation
|
||
let hash = "$5$bx7fjhlS8yMPM3Nc$yRgB5vyoTWeRcYn31RFTg5hAGyTInUq.l0HqLKzRuRC";
|
||
|
||
verify_crypt_pw(phrase, hash).expect("could not verify test password");
|
||
}
|