Merge branch 'crypt' into 'master'

Add Crypt functions

See merge request redox-os/relibc!453
This commit is contained in:
Jeremy Soller
2024-03-01 00:23:42 +00:00
30 changed files with 1180 additions and 11 deletions
+90
View File
@@ -0,0 +1,90 @@
use crate::platform::types::{c_uchar, c_uint};
use alloc::{string::String, vec::Vec};
use base64ct::{Base64Bcrypt, Encoding};
use bcrypt_pbkdf::bcrypt_pbkdf;
use core::str;
const MIN_COST: u32 = 4;
const MAX_COST: u32 = 31;
const BHASH_WORDS: usize = 8;
const BHASH_OUTPUT_SIZE: usize = BHASH_WORDS * 4;
/// Inspired by https://github.com/Keats/rust-bcrypt/blob/87fc59e917bcb6cf3f3752fc7f2b4c659d415597/src/lib.rs#L135
fn split_with_prefix(hash: &str) -> Option<(&str, &str, c_uint)> {
let valid_prefixes = ["2y", "2b", "2a", "2x"];
// Should be [prefix, cost, hash]
let raw_parts: Vec<_> = hash.split('$').skip(1).collect();
if raw_parts.len() != 3 {
return None;
}
let prefix = raw_parts[0];
let setting = raw_parts[2];
if !valid_prefixes.contains(&prefix) {
return None;
}
raw_parts[1]
.parse::<c_uint>()
.ok()
.map(|cost| (prefix, setting, cost))
}
/// Performs Blowfish key derivation on a given password with a specific setting.
///
/// # Parameters
/// * `passw`: The password to be hashed. It must be a string slice (`&str`).
/// * `setting`: The settings for the Blowfish key derivation. It must be a string slice (`&str`)
/// and should follow the format `$<prefix>$<cost>$<setting>`, where `<prefix>` is a string that
/// indicates the type of the hash (e.g., "$2a$"), `<cost>` is a decimal number representing
/// the cost factor for the Blowfish operation, and `<setting>` is a base64-encoded string
/// representing the salt to be used for the Blowfish function.
///
/// # Returns
/// * `Option<String>`: Returns `Some(String)` if the Blowfish operation was successful, where the
/// returned string is the result of the Blowfish operation formatted according to the Modular
/// Crypt Format (MCF). If the Blowfish operation failed, it returns `None`.
///
/// # Errors
/// * If the cost factor is outside the range `[MIN_COST, MAX_COST]`.
///
/// # Example
/// ```
/// let password = "correctbatteryhorsestapler";
/// let setting = "$2y$12$L6Bc/AlTQHyd9liGgGEZyO";
/// let result = crypt_blowfish(password, setting);
/// assert!(result.is_some());
///```
///
/// # Note
/// The `crypt_blowfish` function uses the Blowfish block cipher for hashing.
/// The output of the Blowfish operation is base64-encoded using the BCrypt variant of base64.
pub fn crypt_blowfish(passw: &str, setting: &str) -> Option<String> {
if let Some((prefix, setting, cost)) = split_with_prefix(setting) {
if !(MIN_COST..=MAX_COST).contains(&cost) {
return None;
}
// Passwords need to be null terminated
let mut vec = Vec::with_capacity(passw.len() + 1);
vec.extend_from_slice(passw.as_bytes());
vec.push(0);
// We only consider the first 72 chars; truncate if necessary.
let passw_t = if vec.len() > 72 { &vec[..72] } else { &vec };
let passw: &[c_uchar] = &passw_t;
let setting = setting.as_bytes();
let mut output = vec![0; BHASH_OUTPUT_SIZE + 1];
bcrypt_pbkdf(passw, setting, cost, &mut output).ok()?;
Some(format!(
"${}${}${}",
prefix,
cost,
Base64Bcrypt::encode_string(&output),
))
} else {
None
}
}
+9
View File
@@ -0,0 +1,9 @@
include_guard = "_RELIBC_CRYPT_H"
language = "C"
style = "Type"
no_includes = true
cpp_compat = true
[enum]
prefix_with_name = true
+146
View File
@@ -0,0 +1,146 @@
use crate::platform::types::c_uchar;
use alloc::string::String;
use base64ct::{Base64ShaCrypt, Encoding};
use core::str;
use md5_crypto::{Digest, Md5};
// Block size for MD5
const BLOCK_SIZE: usize = 16;
// PWD part length of the password string
const PW_SIZE_MD5: usize = 22;
// Maximum length of a setting
const SALT_MAX: usize = 8;
// Inverse encoding map for MD5.
const MAP_MD5: [c_uchar; BLOCK_SIZE] = [12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11];
const KEY_MAX: usize = 30000;
fn encode_md5(source: &[c_uchar]) -> Option<[c_uchar; PW_SIZE_MD5]> {
let mut transposed = [0; BLOCK_SIZE];
for (i, &ti) in MAP_MD5.iter().enumerate() {
transposed[i] = source[ti as usize];
}
let mut buf = [0; PW_SIZE_MD5];
Base64ShaCrypt::encode(&transposed, &mut buf).ok()?;
Some(buf)
}
/// Function taken from PR: https://github.com/RustCrypto/password-hashes/pull/351
/// This won't be needed once the PR is merged
fn inner_md5(passw: &str, setting: &str) -> Option<String> {
let mut digest_b = Md5::default();
digest_b.update(passw);
digest_b.update(setting);
digest_b.update(passw);
let hash_b = digest_b.finalize();
let mut digest_a = Md5::default();
digest_a.update(passw);
digest_a.update("$1$");
digest_a.update(setting);
let mut pw_len = passw.len();
let rounds = pw_len / BLOCK_SIZE;
for _ in 0..rounds {
digest_a.update(hash_b);
}
// leftover passw
digest_a.update(&hash_b[..(pw_len - rounds * BLOCK_SIZE)]);
while pw_len > 0 {
match pw_len & 1 {
0 => digest_a.update(&passw[..1]),
1 => digest_a.update([0u8]),
_ => unreachable!(),
}
pw_len >>= 1;
}
let mut hash_a = digest_a.finalize();
// Repeatedly run the collected hash value through MD5 to burn
// CPU cycles
for i in 0..1000_usize {
// new hasher
let mut hasher = Md5::default();
// Add key or last result
if (i & 1) != 0 {
hasher.update(passw);
} else {
hasher.update(hash_a);
}
// Add setting for numbers not divisible by 3
if i % 3 != 0 {
hasher.update(setting);
}
// Add key for numbers not divisible by 7
if i % 7 != 0 {
hasher.update(passw);
}
// Add key or last result
if (i & 1) != 0 {
hasher.update(hash_a);
} else {
hasher.update(passw);
}
// digest_c.clone_from_slice(&hasher.finalize());
hash_a = hasher.finalize();
}
encode_md5(hash_a.as_slice())
.map(|encstr| format!("$1${}${}", setting, str::from_utf8(&encstr).unwrap()))
}
/// Performs MD5 hashing on a given password with a specific setting.
///
/// # Parameters
/// * `passw`: The password to be hashed. It must be a string slice (`&str`).
/// * `setting`: The settings for the MD5 hashing. It must be a string slice (`&str`)
/// and should start with "$1$". The rest of the string should represent the salt
/// to be used for the MD5 hashing.
///
/// # Returns
/// * `Option<String>`: Returns `Some(String)` if the MD5 operation was successful, where the
/// returned string is the result of the MD5 operation formatted according to the Modular
/// Crypt Format (MCF). If the MD5 operation failed, it returns `None`.
///
/// # Errors
/// * If the `passw` length exceeds `KEY_MAX`.
/// * If the `setting` does not start with "$1$".
///
/// # Example
/// ```
/// let password = "my_password";
/// let setting = "$1$saltstring";
/// let result = crypt_md5(password, setting);
/// assert!(result.is_some());
/// ```
///
/// # Note
/// The `crypt_md5` function uses the MD5 hashing algorithm for hashing.
/// The output of the MD5 operation is base64-encoded using the BCrypt variant of base64.
pub fn crypt_md5(passw: &str, setting: &str) -> Option<String> {
/* reject large keys */
if passw.len() > KEY_MAX {
return None;
}
if &setting[0..3] != "$1$" {
return None;
}
let cursor = 3;
let slen = cursor
+ setting[cursor..cursor + SALT_MAX]
.chars()
.take_while(|c| *c != '$')
.count();
let setting = &setting[cursor..slen];
inner_md5(passw, setting)
}
+101
View File
@@ -0,0 +1,101 @@
use ::scrypt::password_hash::{Salt, SaltString};
use alloc::{
ffi::CString,
string::{String, ToString},
};
use core::ptr;
use rand::{rngs::SmallRng, RngCore, SeedableRng};
use crate::{
c_str::CStr,
header::{errno::EINVAL, stdlib::rand},
platform::{self, types::*},
};
mod blowfish;
mod md5;
mod pbkdf2;
mod scrypt;
mod sha;
use self::{
blowfish::crypt_blowfish,
md5::crypt_md5,
pbkdf2::crypt_pbkdf2,
scrypt::crypt_scrypt,
sha::{crypt_sha, ShaType::*},
};
#[repr(C)]
pub struct crypt_data {
initialized: c_int,
buff: [c_char; 256],
}
impl crypt_data {
pub fn new() -> Self {
crypt_data {
initialized: 1,
buff: [0; 256],
}
}
}
fn gen_salt() -> Option<String> {
let mut rng = SmallRng::seed_from_u64(unsafe { rand() as u64 });
let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH];
rng.fill_bytes(&mut bytes);
Some(SaltString::encode_b64(&bytes).ok()?.as_str().to_string())
}
#[no_mangle]
pub unsafe extern "C" fn crypt_r(
key: *const c_char,
setting: *const c_char,
data: *mut crypt_data,
) -> *mut c_char {
if (*data).initialized == 0 {
*data = crypt_data::new();
}
let key = CStr::from_ptr(key).to_str().expect("key must be utf-8");
let setting = CStr::from_ptr(setting)
.to_str()
.expect("setting must be utf-8");
let setting_bytes = setting.as_bytes();
let encoded = if setting_bytes[0] == b'$' && setting_bytes[1] != 0 && setting_bytes[2] != 0 {
if setting_bytes[1] == b'1' && setting_bytes[2] == b'$' {
crypt_md5(key, setting)
} else if setting_bytes[1] == b'2' && setting_bytes[3] == b'$' {
crypt_blowfish(key, setting)
} else if setting_bytes[1] == b'5' && setting_bytes[2] == b'$' {
crypt_sha(key, setting, Sha256)
} else if setting_bytes[1] == b'6' && setting_bytes[2] == b'$' {
crypt_sha(key, setting, Sha512)
} else if setting_bytes[1] == b'7' && setting_bytes[2] == b'$' {
crypt_scrypt(key, setting)
} else if setting_bytes[1] == b'8' && setting_bytes[2] == b'$' {
crypt_pbkdf2(key, setting)
} else {
platform::errno = EINVAL;
return ptr::null_mut();
}
} else {
None
};
if let Some(inner) = encoded {
let len = inner.len();
if let Ok(ret) = CString::new(inner) {
let ret_ptr = ret.into_raw();
let dst = (*data).buff.as_mut_ptr();
ptr::copy_nonoverlapping(ret_ptr, dst.cast(), len);
ret_ptr.cast()
} else {
ptr::null_mut()
}
} else {
ptr::null_mut()
}
}
+64
View File
@@ -0,0 +1,64 @@
use super::gen_salt;
use alloc::string::{String, ToString};
use base64ct::{Base64Bcrypt, Encoding};
use core::{str, u32};
use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;
/// Performs PBKDF2 key derivation on a given password with a specific setting.
///
/// # Parameters
/// * `passw`: The password to be hashed. It must be a string slice (`&str`).
/// * `setting`: The settings for the PBKDF2 key derivation. It must be a string slice (`&str`)
/// and should follow the format `$<iter>$<salt>`. The `<iter>` part should be a hexadecimal
/// number representing the iteration count for the PBKDF2 function. The `<salt>` part should
/// be a base64-encoded string representing the salt to be used for the PBKDF2 function.
///
/// # Returns
/// * `Option<String>`: Returns `Some(String)` if the PBKDF2 operation was successful, where the
/// returned string is the result of the PBKDF2 operation formatted according to the Modular
/// Crypt Format (MCF). If the PBKDF2 operation failed, it returns `None`.
///
/// # Errors
/// * If the `setting` does not contain a '$' character.
/// * If the `setting` contains another '$' character after the first one.
/// * If the `<salt>` part of the `setting` is empty.
/// * If the `<iter>` part of the `setting` cannot be converted into a `u32` integer.
///
/// # Example
/// ```
/// let password = "my_password";
/// let setting = "$8$3e8$salt";
/// let result = crypt_pbkdf2(password, setting);
/// assert!(result.is_some());
/// ```
///
/// # Note
/// The `crypt_pbkdf2` function uses the SHA256 hashing algorithm for the PBKDF2 operation.
/// The output of the PBKDF2 operation is base64-encoded using the BCrypt variant of base64.
pub fn crypt_pbkdf2(passw: &str, setting: &str) -> Option<String> {
if let Some((iter_str, salt)) = &setting[3..].split_once('$') {
if salt.contains('$') {
return None;
}
let actual_salt = if salt.len() > 0 {
salt.to_string()
} else {
gen_salt()?
};
let iter = u32::from_str_radix(iter_str, 16).ok()?;
let mut buffer = [0u8; 32];
pbkdf2_hmac::<Sha256>(passw.as_bytes(), actual_salt.as_bytes(), iter, &mut buffer);
Some(format!(
"$8${}${}${}",
iter_str,
salt,
Base64Bcrypt::encode_string(&buffer)
))
} else {
None
}
}
+115
View File
@@ -0,0 +1,115 @@
use super::gen_salt;
use crate::platform::types::*;
use alloc::string::{String, ToString};
use base64ct::{Base64Bcrypt, Encoding};
use core::{str, u32};
use scrypt::{scrypt, Params};
/// Map for encoding and decoding
#[inline(always)]
fn to_digit(c: char, radix: u32) -> Option<u32> {
match c {
'.' => Some(0),
'/' => Some(1),
_ => c.to_digit(radix).map(|d| d + 2),
}
}
/// Decodes a 5 character lengt str value to c_uint
///
/// # Arguments
///
/// * `value` - A string slice that represents a u32 value in base64
///
/// # Returns
///
/// * `Option<c_uint>` - Returns the decoded c_uint value if successful, otherwise None
fn dencode_uint(value: &str) -> Option<c_uint> {
if value.len() != 5 {
return None;
}
let result = value
.chars()
.enumerate()
.try_fold(0 as c_uint, |acc, (i, c)| {
acc.checked_add((to_digit(c, 30)? as c_uint) << (i * 6))
});
Some(result?)
}
/// Reads settings for password encryption
///
/// # Arguments
///
/// * `setting` - A string slice that represents the settings
///
/// # Returns
///
/// * `Option<(c_uchar, c_uint, c_uint, String)>` - Returns a tuple containing the settings if successful, otherwise None
fn read_setting(setting: &str) -> Option<(c_uchar, c_uint, c_uint, String)> {
let nlog2 = to_digit(setting.chars().next()?, 30)? as c_uchar;
let r = dencode_uint(&setting[1..6])?;
let p = dencode_uint(&setting[6..11])?;
let salt = &setting[11..];
let actual_salt = if salt.len() > 0 {
salt.to_string()
} else {
gen_salt()?
};
Some((nlog2, r, p, actual_salt))
}
/// Performs Scrypt key derivation on a given password with a specific setting.
///
/// # Parameters
/// * `passw`: The password to be hashed. It must be a string slice (`&str`).
/// * `setting`: The settings for the Scrypt key derivation. It must be a string slice (`&str`)
/// and should follow the format `$<Nlog2>$<r>$<p>$<salt>`. The `<Nlog2>` part should be a decimal
/// number representing the logarithm base 2 of the CPU/memory cost factor N for Scrypt. The `<r>`
/// part should be a decimal number representing the block size r. The `<p>` part should be a decimal
/// number representing the parallelization factor p. The `<salt>` part should be a base64-encoded
/// string representing the salt to be used for the Scrypt function.
///
/// # Returns
/// * `Option<String>`: Returns `Some(String)` if the Scrypt operation was successful, where the
/// returned string is the result of the Scrypt operation formatted according to the Modular
/// Crypt Format (MCF). If the Scrypt operation failed, it returns `None`.
///
/// # Errors
/// * If the `setting` length is less than 14 characters.
/// * If the `scrypt` function fails to perform the Scrypt operation.
///
/// # Example
/// ```
/// let password = "my_password";
/// let setting = "$7$C6..../....SodiumChloride";
/// let result = crypt_scrypt(password, setting);
/// assert!(result.is_some());
/// ```
///
/// # Note
/// The `crypt_scrypt` function uses the Scrypt key derivation function for hashing.
/// The output of the Scrypt operation is base64-encoded using the BCrypt variant of base64.
pub fn crypt_scrypt(passw: &str, setting: &str) -> Option<String> {
if setting.len() < 14 {
return None;
}
let (nlog2, r, p, salt) = read_setting(&setting[3..])?;
let params = Params::new(nlog2, r, p, 32).ok()?;
let mut output = [0u8; 32];
scrypt(passw.as_bytes(), salt.as_bytes(), &params, &mut output).ok()?;
Some(format!(
"$7${}${}${}",
&setting[3..14],
salt,
Base64Bcrypt::encode_string(&output)
))
}
+141
View File
@@ -0,0 +1,141 @@
use alloc::string::{String, ToString};
use sha_crypt::{
sha256_crypt_b64, sha512_crypt_b64, Sha256Params, Sha512Params, ROUNDS_DEFAULT, ROUNDS_MAX,
ROUNDS_MIN,
};
use crate::platform::types::*;
// key limit is not part of the original design, added for DoS protection.
// rounds limit has been lowered (versus the reference/spec), also for DoS
// protection. runtime is O(klen^2 + klen*rounds)
const KEY_MAX: usize = 256;
const SALT_MAX: usize = 16;
const RSTRING: &str = "rounds=";
pub enum ShaType {
Sha256,
Sha512,
}
/// Performs SHA hashing on a given password with a specific setting.
///
/// # Parameters
/// * `passw`: The password to be hashed. It must be a string slice (`&str`).
/// * `setting`: The settings for the SHA hashing. It must be a string slice (`&str`)
/// and should start with "$5$" for SHA256 or "$6$" for SHA512. The rest of the string should represent the salt
/// to be used for the SHA hashing.
/// * `cipher`: The type of SHA algorithm to use. It should be either `ShaType::Sha256` or `ShaType::Sha512`.
///
/// # Returns
/// * `Option<String>`: Returns `Some(String)` if the SHA operation was successful, where the
/// returned string is the result of the SHA operation formatted according to the Modular
/// Crypt Format (MCF). If the SHA operation failed, it returns `None`.
///
/// # Errors
/// * If the `passw` length exceeds `KEY_MAX`.
/// * If the `setting` does not start with "$5$" or "$6$".
/// * If the `setting` does not contain a '$' character.
/// * If the `setting` contains another '$' character after the first one.
/// * If the `setting` contains invalid characters.
/// * If the `setting` contains an invalid number of rounds.
/// * If the `sha256_crypt_b64` or `sha512_crypt_b64` function fails to hash the password.
///
/// # Example
/// ```
/// let password = "my_password";
/// let setting = "$5$rounds=1400$anotherlongsaltstringg";
/// let result = crypt_sha(password, setting, ShaType::Sha256);
/// assert!(result.is_some());
/// ```
///
/// # Note
/// The `crypt_sha` function uses the SHA256 or SHA512 hashing algorithm for hashing.
/// The output of the SHA operation is base64-encoded using the BCrypt variant of base64.
pub fn crypt_sha(passw: &str, setting: &str, cipher: ShaType) -> Option<String> {
let mut cursor = 3;
let rounds;
/* reject large keys */
if passw.len() > KEY_MAX {
return None;
}
// SHA256
// setting: $5$rounds=n$setting$ (rounds=n$ and closing $ are optional)
// SHA512
// setting: $6$rounds=n$setting$ (rounds=n$ and closing $ are optional)
let param = match cipher {
ShaType::Sha256 => "$5$",
ShaType::Sha512 => "$6$",
};
if &setting[0..3] != param {
return None;
}
let has_round;
// 7 is len("rounds=")
if &setting[cursor..cursor + 7] == RSTRING {
cursor += 7;
has_round = true;
if let Some(c_end) = setting[cursor..].chars().position(|r| r == '$') {
if let Ok(u) = setting[cursor..cursor + c_end].parse::<c_ulong>() {
cursor += c_end + 1;
rounds = u.min(ROUNDS_MAX as c_ulong).max(ROUNDS_MIN as c_ulong);
} else {
return None;
}
} else {
return None;
}
} else {
has_round = false;
rounds = ROUNDS_DEFAULT as c_ulong;
}
let mut slen = cursor;
for i in 0..SALT_MAX.min(setting.len() - cursor) {
let idx = cursor + i;
if &setting[idx..idx + 1] == "$" {
break;
}
// reject characters that interfere with /etc/shadow parsing
if &setting[idx..idx + 1] == "\n" || &setting[idx..idx + 1] == ":" {
return None;
}
slen += 1;
}
let setting = &setting[cursor..slen];
if let Ok(enc) = match cipher {
ShaType::Sha256 => {
let params = Sha256Params::new(rounds as usize)
.unwrap_or(Sha256Params::new(ROUNDS_DEFAULT).unwrap());
sha256_crypt_b64(passw.as_bytes(), setting.as_bytes(), &params)
}
ShaType::Sha512 => {
let params = Sha512Params::new(rounds as usize)
.unwrap_or(Sha512Params::new(ROUNDS_DEFAULT).unwrap());
sha512_crypt_b64(passw.as_bytes(), setting.as_bytes(), &params)
}
} {
let (r_slice, rn_slice) = if has_round {
(RSTRING, rounds.to_string() + "$")
} else {
("", String::new())
};
Some(format!(
"{}{}{}{}${}",
param, r_slice, rn_slice, setting, enc
))
} else {
None
}
}
+1
View File
@@ -4,6 +4,7 @@ pub mod arpa_inet;
pub mod assert;
pub mod bits_pthread;
pub mod bits_sched;
pub mod crypt;
pub mod ctype;
pub mod dirent;
#[path = "dl-tls/mod.rs"]
+9 -5
View File
@@ -10,8 +10,11 @@ use core::{
use crate::{
c_str::CStr,
header::{
errno, fcntl, limits, stdlib::getenv, sys_ioctl, sys_resource, sys_time, sys_utsname,
termios, time::timespec,
crypt::{crypt_data, crypt_r},
errno, fcntl, limits,
stdlib::getenv,
sys_ioctl, sys_resource, sys_time, sys_utsname, termios,
time::timespec,
},
platform::{self, types::*, Pal, Sys},
};
@@ -119,9 +122,10 @@ pub extern "C" fn confstr(name: c_int, buf: *mut c_char, len: size_t) -> size_t
unimplemented!();
}
// #[no_mangle]
pub extern "C" fn crypt(key: *const c_char, salt: *const c_char) -> *mut c_char {
unimplemented!();
#[no_mangle]
pub unsafe extern "C" fn crypt(key: *const c_char, salt: *const c_char) -> *mut c_char {
let mut data = crypt_data::new();
crypt_r(key, salt, &mut data as *mut _)
}
#[no_mangle]