use std::{
alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout},
ffi::{c_char, c_ulong, CStr, CString},
fmt, io,
};
#[derive(Debug)]
pub enum Error {
InvalidArgument(String),
PhraseTooLong,
RngNotAvailable,
IoError(io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::InvalidArgument(ref msg) => write!(f, "{msg}"),
Self::PhraseTooLong => {
write!(f, "Input phrase is too long for specified hashing method")
}
Self::RngNotAvailable => {
write!(f, "No random number generator is available on the platform")
}
Self::IoError(..) => {
write!(f, "An unknown IO error occured")
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::IoError(ref e) => Some(e),
_ => None,
}
}
}
impl Error {
fn invalid_argument(msg: &str) -> Error {
Self::InvalidArgument(msg.into())
}
}
pub fn crypt_gensalt(
prefix: Option<&str>,
count: c_ulong,
random_bytes: Option<&[u8]>,
) -> Result<String, Error> {
let c_prefix = prefix
.map(|s| CString::new(s).map_err(|_| Error::invalid_argument("Prefix contains NULL byte")))
.transpose()?;
let c_prefix_ptr = match &c_prefix {
Some(cs) => cs.as_ptr(),
None => std::ptr::null(),
};
let rbytes_ptr = match &random_bytes {
Some(rb) => rb.as_ptr().cast::<c_char>(),
None => std::ptr::null(),
};
let nrbytes = random_bytes
.as_ref()
.map_or(0, |rb| rb.len())
.try_into()
.map_err(|_| Error::invalid_argument("Too many random bytes"))?;
let mut output = [0; xcrypt_sys::CRYPT_GENSALT_OUTPUT_SIZE as usize];
let output_len = output.len().try_into().map_err(|_| {
Error::invalid_argument(
"Output buffer is too big. This is an internal error and should never occur",
)
})?;
let c_settings = unsafe {
let settings_ptr = xcrypt_sys::crypt_gensalt_rn(
c_prefix_ptr,
count,
rbytes_ptr,
nrbytes,
output.as_mut_ptr(),
output_len,
);
if settings_ptr.is_null() {
let last_os_error = io::Error::last_os_error();
if let Some(errno) = last_os_error.raw_os_error() {
let error = match errno {
22 => Error::invalid_argument("Invalid prefix, count, or random_bytes"),
88 | 13 | 5 => Error::RngNotAvailable,
_ => Error::IoError(last_os_error),
};
return Err(error);
}
}
CStr::from_ptr(settings_ptr)
};
Ok(c_settings.to_string_lossy().to_string())
}
struct CryptData {
ptr: *mut u8,
layout: Layout,
}
impl CryptData {
fn new() -> Self {
unsafe {
let layout = Layout::new::<xcrypt_sys::crypt_data>();
let ptr = alloc_zeroed(layout);
if ptr.is_null() {
handle_alloc_error(layout);
}
Self { ptr, layout }
}
}
fn as_ptr(&self) -> *mut xcrypt_sys::crypt_data {
self.ptr.cast::<xcrypt_sys::crypt_data>()
}
}
impl Drop for CryptData {
fn drop(&mut self) {
unsafe {
dealloc(self.ptr, self.layout);
}
}
}
pub fn crypt(phrase: &str, setting: &str) -> Result<String, Error> {
let c_phrase =
CString::new(phrase).map_err(|_| Error::invalid_argument("Phrase contains NULL byte"))?;
let c_setting =
CString::new(setting).map_err(|_| Error::invalid_argument("Setting contains NULL byte"))?;
let hashed_phrase = unsafe {
let crypt_data = CryptData::new();
let hashed_phrase_ptr =
xcrypt_sys::crypt_r(c_phrase.as_ptr(), c_setting.as_ptr(), crypt_data.as_ptr());
if hashed_phrase_ptr.is_null() {
let last_os_error = io::Error::last_os_error();
if let Some(errno) = last_os_error.raw_os_error() {
let error = match errno {
22 => Error::invalid_argument("Invalid setting"),
34 => Error::PhraseTooLong,
_ => Error::IoError(last_os_error),
};
return Err(error);
}
}
CStr::from_ptr(hashed_phrase_ptr)
.to_string_lossy()
.to_string()
};
Ok(hashed_phrase)
}