From 38d9c05e3967839107480a065ef739bf89d48a60 Mon Sep 17 00:00:00 2001 From: lyw458372 Date: Tue, 9 Jun 2026 15:23:50 +0800 Subject: [PATCH] fix: crypt_r: treat key as raw bytes instead of requiring UTF-8 --- src/header/crypt/argon2.rs | 4 ++-- src/header/crypt/blowfish.rs | 4 ++-- src/header/crypt/md5.rs | 4 ++-- src/header/crypt/mod.rs | 14 ++++++++------ src/header/crypt/pbkdf2.rs | 4 ++-- src/header/crypt/scrypt.rs | 4 ++-- src/header/crypt/sha.rs | 6 +++--- tests/crypt/scrypt.c | 18 ++++++++++++++++-- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/header/crypt/argon2.rs b/src/header/crypt/argon2.rs index 99b7e57e2d..c0aa2c22ae 100644 --- a/src/header/crypt/argon2.rs +++ b/src/header/crypt/argon2.rs @@ -4,11 +4,11 @@ use argon2::{ password_hash::{PasswordHash, PasswordVerifier}, }; -pub fn crypt_argon2(key: &str, setting: &str) -> Option { +pub fn crypt_argon2(key: &[u8], setting: &str) -> Option { let hash = PasswordHash::new(setting).ok()?; let argon2 = Argon2::default(); - if argon2.verify_password(key.as_bytes(), &hash).is_ok() { + if argon2.verify_password(key, &hash).is_ok() { Some(setting.to_string()) } else { None diff --git a/src/header/crypt/blowfish.rs b/src/header/crypt/blowfish.rs index 54e9fe1082..af44b246dd 100644 --- a/src/header/crypt/blowfish.rs +++ b/src/header/crypt/blowfish.rs @@ -61,14 +61,14 @@ fn split_with_prefix(hash: &str) -> Option<(&str, &str, c_uint)> { /// # 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 { +pub fn crypt_blowfish(passw: &[u8], setting: &str) -> Option { 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.extend_from_slice(passw); vec.push(0); // We only consider the first 72 chars; truncate if necessary. diff --git a/src/header/crypt/md5.rs b/src/header/crypt/md5.rs index 25cfacff37..eabbb9e467 100644 --- a/src/header/crypt/md5.rs +++ b/src/header/crypt/md5.rs @@ -27,7 +27,7 @@ fn encode_md5(source: &[c_uchar]) -> Option<[c_uchar; PW_SIZE_MD5]> { /// 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 { +fn inner_md5(passw: &[u8], setting: &str) -> Option { let mut digest_b = Md5::default(); digest_b.update(passw); digest_b.update(setting); @@ -124,7 +124,7 @@ fn inner_md5(passw: &str, setting: &str) -> Option { /// # 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 { +pub fn crypt_md5(passw: &[u8], setting: &str) -> Option { /* reject large keys */ if passw.len() > KEY_MAX { return None; diff --git a/src/header/crypt/mod.rs b/src/header/crypt/mod.rs index 4a3edd6872..d66b467d96 100644 --- a/src/header/crypt/mod.rs +++ b/src/header/crypt/mod.rs @@ -73,12 +73,14 @@ pub unsafe extern "C" fn crypt_r( unsafe { *data = crypt_data::new() }; } - let key = unsafe { CStr::from_ptr(key) } - .to_str() - .expect("key must be utf-8"); - let setting = unsafe { CStr::from_ptr(setting) } - .to_str() - .expect("setting must be utf-8"); + let key = unsafe { CStr::from_ptr(key) }.to_bytes(); + let setting = match unsafe { CStr::from_ptr(setting) }.to_str() { + Ok(s) => s, + Err(_) => { + platform::ERRNO.set(EINVAL); + return ptr::null_mut(); + } + }; let encoded = if setting.starts_with('$') { if setting.starts_with("$1$") { diff --git a/src/header/crypt/pbkdf2.rs b/src/header/crypt/pbkdf2.rs index 9a614873e6..53d53ce685 100644 --- a/src/header/crypt/pbkdf2.rs +++ b/src/header/crypt/pbkdf2.rs @@ -36,7 +36,7 @@ use sha2::Sha256; /// # 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 { +pub fn crypt_pbkdf2(passw: &[u8], setting: &str) -> Option { if let Some((iter_str, salt)) = &setting[3..].split_once('$') { if salt.contains('$') { return None; @@ -50,7 +50,7 @@ pub fn crypt_pbkdf2(passw: &str, setting: &str) -> Option { let iter = u32::from_str_radix(iter_str, 16).ok()?; let mut buffer = [0u8; 32]; - pbkdf2_hmac::(passw.as_bytes(), actual_salt.as_bytes(), iter, &mut buffer); + pbkdf2_hmac::(passw, actual_salt.as_bytes(), iter, &mut buffer); Some(format!( "$8${}${}${}", diff --git a/src/header/crypt/scrypt.rs b/src/header/crypt/scrypt.rs index 334d9dfdb4..55437b84a2 100644 --- a/src/header/crypt/scrypt.rs +++ b/src/header/crypt/scrypt.rs @@ -92,7 +92,7 @@ fn read_setting(setting: &str) -> Option<(c_uchar, c_uint, c_uint, String)> { /// # 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 { +pub fn crypt_scrypt(passw: &[u8], setting: &str) -> Option { if setting.len() < 14 { return None; } @@ -102,7 +102,7 @@ pub fn crypt_scrypt(passw: &str, setting: &str) -> Option { let params = Params::new(nlog2, r, p, 32).ok()?; let mut output = [0u8; 32]; - scrypt(passw.as_bytes(), salt.as_bytes(), ¶ms, &mut output).ok()?; + scrypt(passw, salt.as_bytes(), ¶ms, &mut output).ok()?; Some(format!( "$7${}${}${}", diff --git a/src/header/crypt/sha.rs b/src/header/crypt/sha.rs index 50dca3563a..01b339f944 100644 --- a/src/header/crypt/sha.rs +++ b/src/header/crypt/sha.rs @@ -53,7 +53,7 @@ pub enum ShaType { /// # 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 { +pub fn crypt_sha(passw: &[u8], setting: &str, cipher: ShaType) -> Option { let mut cursor = 3; let rounds; @@ -117,12 +117,12 @@ pub fn crypt_sha(passw: &str, setting: &str, cipher: ShaType) -> Option 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(), ¶ms) + sha256_crypt_b64(passw, setting.as_bytes(), ¶ms) } 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(), ¶ms) + sha512_crypt_b64(passw, setting.as_bytes(), ¶ms) } } { let (r_slice, rn_slice) = if has_round { diff --git a/tests/crypt/scrypt.c b/tests/crypt/scrypt.c index d6f70778ce..207b46f4c7 100644 --- a/tests/crypt/scrypt.c +++ b/tests/crypt/scrypt.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -12,14 +14,26 @@ int main() // No salt result = crypt("pleaseletmein", "$7$C6..../...."); assert(result != NULL); - + // Invalid encoded number for r result = crypt("pleaseletmein", "$7$C6.../....SodiumChloride"); assert(result == NULL); - + // Invalid encoded number for p result = crypt("pleaseletmein", "$7$C6..../...SodiumChloride"); assert(result == NULL); + // Non-UTF-8 key should succeed (treated as raw bytes) + char key[] = {(char)0xC0, (char)0x01, '\0'}; + result = crypt(key, "$5$saltsalt$"); + assert(result != NULL); + + // Non-UTF-8 setting should return NULL with errno=EINVAL + char setting[] = {'$', '5', '$', (char)0xFE, (char)0xFF, '$', '\0'}; + errno = 0; + result = crypt("password", setting); + assert(result == NULL); + assert(errno == EINVAL); + return 0; }