#![no_std]
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![warn(unreachable_pub)]
#![deny(unsafe_op_in_unsafe_fn)]
#[cfg(not(any(
feature = "backend-aws-lc",
feature = "backend-boring",
feature = "backend-openssl",
feature = "backend-rust-crypto",
)))]
compile_error!(
"crypt-sha512: no backend selected. Enable exactly one of the cargo features: \
`backend-aws-lc`, `backend-boring`, `backend-openssl`, `backend-rust-crypto`."
);
#[cfg(any(
all(feature = "backend-aws-lc", feature = "backend-boring"),
all(feature = "backend-aws-lc", feature = "backend-openssl"),
all(feature = "backend-aws-lc", feature = "backend-rust-crypto"),
all(feature = "backend-boring", feature = "backend-openssl"),
all(feature = "backend-boring", feature = "backend-rust-crypto"),
all(feature = "backend-openssl", feature = "backend-rust-crypto"),
))]
compile_error!(
"crypt-sha512: more than one backend selected. The `backend-*` features are \
mutually exclusive; enable exactly one of \
`backend-aws-lc`, `backend-boring`, `backend-openssl`, `backend-rust-crypto`."
);
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
mod backend;
#[cfg(any(
feature = "backend-aws-lc",
feature = "backend-boring",
feature = "backend-openssl",
feature = "backend-rust-crypto",
))]
use crate::backend as crypto;
#[cfg(any(
feature = "backend-aws-lc",
feature = "backend-boring",
feature = "backend-openssl",
feature = "backend-rust-crypto",
))]
use crate::backend::Sha512Context;
const SHA512_SALT_PREFIX_STR: &str = "$6$";
const SHA512_SALT_PREFIX: &[u8] = SHA512_SALT_PREFIX_STR.as_bytes();
const SHA512_ROUNDS_PREFIX: &[u8] = b"rounds=";
const SALT_LEN_MAX: usize = 16;
const ROUNDS_DEFAULT: u32 = 5000;
const ROUNDS_MIN: u32 = 1000;
const ROUNDS_MAX: u32 = 999_999_999;
const SHA512_HASH_ENCODED_LENGTH: usize = 86;
const B64_CHARS: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub struct Password {
bytes: Vec<u8>,
}
impl Password {
#[inline]
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self { bytes }
}
#[inline]
fn into_bytes(self) -> Vec<u8> {
let mut me = core::mem::ManuallyDrop::new(self);
core::mem::take(&mut me.bytes)
}
}
impl From<String> for Password {
#[inline]
fn from(s: String) -> Self {
Self {
bytes: s.into_bytes(),
}
}
}
impl From<&str> for Password {
#[inline]
fn from(s: &str) -> Self {
Self {
bytes: s.as_bytes().to_vec(),
}
}
}
impl From<Vec<u8>> for Password {
#[inline]
fn from(bytes: Vec<u8>) -> Self {
Self { bytes }
}
}
impl Drop for Password {
fn drop(&mut self) {
crypto::secure_zero_bytes(&mut self.bytes);
}
}
impl fmt::Debug for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Password").finish_non_exhaustive()
}
}
#[inline(always)]
fn salt_spec_output_size(salt_len: usize, rounds_custom: bool) -> usize {
let mut size = SHA512_SALT_PREFIX.len() + salt_len + 1;
if rounds_custom {
size += SHA512_ROUNDS_PREFIX.len() + 9 + 1;
}
size
}
fn generate_salt(buf: &mut [u8]) {
crypto::random_bytes(buf);
for byte in buf.iter_mut() {
*byte = B64_CHARS[(*byte & 0x3f) as usize];
}
}
#[inline]
fn atoi_u32(ascii: &[u8]) -> Option<u32> {
let mut out: u32 = 0;
for d in ascii.iter().map(|b| b.wrapping_sub(b'0')) {
if d < 10 {
out = out.saturating_mul(10).saturating_add(d as u32);
} else {
return None;
}
}
Some(out)
}
#[inline]
fn push_u32_as_ascii(mut value: u32, output: &mut Vec<u8>) {
if value == 0 {
output.push(b'0');
return;
}
let mut digits = [0u8; 10]; let mut idx = digits.len() - 1;
while value > 0 {
(value, digits[idx]) = (value / 10, (value % 10) as u8 | b'0');
idx -= 1;
}
output.extend_from_slice(&digits[idx + 1..]);
}
macro_rules! b64_from_24bit {
($b2:expr, $b1:expr, $b0:expr, $n:expr, $output:expr) => {{
let mut w = (($b2 as u32) << 16) | (($b1 as u32) << 8) | ($b0 as u32);
for _ in 0..$n {
$output.push(B64_CHARS[(w & 0x3f) as usize]);
w >>= 6;
}
}};
}
#[must_use = "the returned hash string is the result of expensive computation"]
pub fn hash_with_salt(password: Password, salt: &[u8]) -> String {
let mut key_bytes = password.into_bytes();
let out = crypt_inner(&mut key_bytes, salt);
crypto::secure_zero_bytes(&mut key_bytes);
out
}
fn crypt_inner(key_bytes: &mut [u8], salt: &[u8]) -> String {
let mut salt = salt;
let mut rounds = ROUNDS_DEFAULT;
let mut rounds_custom = false;
if salt.starts_with(SHA512_SALT_PREFIX) {
salt = &salt[SHA512_SALT_PREFIX.len()..];
}
if salt.starts_with(SHA512_ROUNDS_PREFIX) {
let rest = &salt[SHA512_ROUNDS_PREFIX.len()..];
if let Some(dollar_pos) = rest.iter().position(|&b| b == b'$') {
if let Some(srounds) = atoi_u32(&rest[..dollar_pos]) {
salt = &rest[dollar_pos + 1..];
rounds = srounds.clamp(ROUNDS_MIN, ROUNDS_MAX);
rounds_custom = true;
}
}
}
let salt_len = salt
.iter()
.position(|&b| b == b'$')
.unwrap_or(salt.len())
.min(SALT_LEN_MAX);
let salt = &salt[..salt_len];
let mut ctx = Sha512Context::new();
let mut result;
ctx.update(key_bytes);
ctx.update(salt);
ctx.update(key_bytes);
result = ctx.finish();
ctx = Sha512Context::new();
ctx.update(key_bytes);
ctx.update(salt);
let mut cnt = key_bytes.len();
while cnt > 64 {
ctx.update(&result[..64]);
cnt -= 64;
}
ctx.update(&result[..cnt]);
cnt = key_bytes.len();
while cnt > 0 {
if (cnt & 1) != 0 {
ctx.update(&result[..64]);
} else {
ctx.update(key_bytes);
}
cnt >>= 1;
}
result = ctx.finish();
ctx = Sha512Context::new();
for _ in 0..key_bytes.len() {
ctx.update(key_bytes);
}
let temp_result = ctx.finish();
let mut p_bytes = Vec::with_capacity(key_bytes.len());
cnt = key_bytes.len();
while cnt >= 64 {
p_bytes.extend_from_slice(&temp_result[..64]);
cnt -= 64;
}
p_bytes.extend_from_slice(&temp_result[..cnt]);
ctx = Sha512Context::new();
for _ in 0..(16 + result[0] as usize) {
ctx.update(salt);
}
let temp_result = ctx.finish();
let mut s_bytes = Vec::with_capacity(salt.len());
cnt = salt.len();
while cnt >= 64 {
s_bytes.extend_from_slice(&temp_result[..64]);
cnt -= 64;
}
s_bytes.extend_from_slice(&temp_result[..cnt]);
for cnt in 0..rounds {
ctx = Sha512Context::new();
if (cnt & 1) != 0 {
ctx.update(&p_bytes);
} else {
ctx.update(&result[..64]);
}
if cnt % 3 != 0 {
ctx.update(&s_bytes);
}
if cnt % 7 != 0 {
ctx.update(&p_bytes);
}
if (cnt & 1) != 0 {
ctx.update(&result[..64]);
} else {
ctx.update(&p_bytes);
}
result = ctx.finish();
}
let output_size = salt_spec_output_size(salt.len(), rounds_custom) + SHA512_HASH_ENCODED_LENGTH;
let mut output: Vec<u8> = Vec::with_capacity(output_size);
output.extend_from_slice(SHA512_SALT_PREFIX);
if rounds_custom {
output.extend_from_slice(SHA512_ROUNDS_PREFIX);
push_u32_as_ascii(rounds, &mut output);
output.push(b'$');
}
output.extend_from_slice(salt);
output.push(b'$');
b64_from_24bit!(result[0], result[21], result[42], 4, &mut output);
b64_from_24bit!(result[22], result[43], result[1], 4, &mut output);
b64_from_24bit!(result[44], result[2], result[23], 4, &mut output);
b64_from_24bit!(result[3], result[24], result[45], 4, &mut output);
b64_from_24bit!(result[25], result[46], result[4], 4, &mut output);
b64_from_24bit!(result[47], result[5], result[26], 4, &mut output);
b64_from_24bit!(result[6], result[27], result[48], 4, &mut output);
b64_from_24bit!(result[28], result[49], result[7], 4, &mut output);
b64_from_24bit!(result[50], result[8], result[29], 4, &mut output);
b64_from_24bit!(result[9], result[30], result[51], 4, &mut output);
b64_from_24bit!(result[31], result[52], result[10], 4, &mut output);
b64_from_24bit!(result[53], result[11], result[32], 4, &mut output);
b64_from_24bit!(result[12], result[33], result[54], 4, &mut output);
b64_from_24bit!(result[34], result[55], result[13], 4, &mut output);
b64_from_24bit!(result[56], result[14], result[35], 4, &mut output);
b64_from_24bit!(result[15], result[36], result[57], 4, &mut output);
b64_from_24bit!(result[37], result[58], result[16], 4, &mut output);
b64_from_24bit!(result[59], result[17], result[38], 4, &mut output);
b64_from_24bit!(result[18], result[39], result[60], 4, &mut output);
b64_from_24bit!(result[40], result[61], result[19], 4, &mut output);
b64_from_24bit!(result[62], result[20], result[41], 4, &mut output);
b64_from_24bit!(0, 0, result[63], 2, &mut output);
crypto::secure_zero_bytes(&mut result);
crypto::secure_zero_bytes(&mut p_bytes);
crypto::secure_zero_bytes(&mut s_bytes);
unsafe { String::from_utf8_unchecked(output) }
}
#[must_use = "the returned hash string is the result of expensive computation"]
pub fn hash(password: Password, rounds: Option<u32>) -> String {
let (r, r_custom) = match rounds {
None | Some(ROUNDS_DEFAULT) => (ROUNDS_DEFAULT, false),
Some(r) => (r, true),
};
let mut salt_spec: Vec<u8> = Vec::with_capacity(salt_spec_output_size(SALT_LEN_MAX, r_custom));
salt_spec.extend_from_slice(SHA512_SALT_PREFIX);
if r_custom {
salt_spec.extend_from_slice(SHA512_ROUNDS_PREFIX);
push_u32_as_ascii(r, &mut salt_spec);
salt_spec.push(b'$');
}
let salt_start = salt_spec.len();
salt_spec.resize(salt_start + SALT_LEN_MAX, 0);
generate_salt(&mut salt_spec[salt_start..]);
hash_with_salt(password, &salt_spec)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InvalidHash;
impl fmt::Display for InvalidHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("not a well-formed SHA512-crypt ($6$) hash")
}
}
impl core::error::Error for InvalidHash {}
pub fn verify(password: Password, hash: &str) -> Result<bool, InvalidHash> {
let rest = hash
.strip_prefix(SHA512_SALT_PREFIX_STR)
.ok_or(InvalidHash)?;
let hash_start = rest.rfind('$').ok_or(InvalidHash)?;
let salt = &hash[..SHA512_SALT_PREFIX.len() + hash_start];
let computed = hash_with_salt(password, salt.as_bytes());
Ok(crypto::constant_time_eq(
computed.as_bytes(),
hash.as_bytes(),
))
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::format;
#[test]
fn test_hello_world_basic() {
let result = hash_with_salt(Password::from("Hello world!"), b"$6$saltstring");
assert_eq!(
result,
"$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1"
);
}
#[test]
fn test_hello_world_with_rounds() {
let result = hash_with_salt(
Password::from("Hello world!"),
b"$6$rounds=10000$saltstringsaltstring",
);
assert_eq!(
result,
"$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."
);
}
#[test]
fn test_long_salt_string() {
let result = hash_with_salt(
Password::from("This is just a test"),
b"$6$rounds=5000$toolongsaltstring",
);
assert_eq!(
result,
"$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0"
);
}
#[test]
fn test_multiline_text() {
let result = hash_with_salt(
Password::from("a very much longer text to encrypt. This one even stretches over morethan one line."),
b"$6$rounds=1400$anotherlongsaltstring"
);
assert_eq!(
result,
"$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1"
);
}
#[test]
fn test_short_salt() {
let result = hash_with_salt(
Password::from("we have a short salt string but not a short password"),
b"$6$rounds=77777$short",
);
assert_eq!(
result,
"$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0"
);
}
#[test]
fn test_short_string() {
let result = hash_with_salt(
Password::from("a short string"),
b"$6$rounds=123456$asaltof16chars..",
);
assert_eq!(
result,
"$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1"
);
}
#[test]
fn test_rounds_minimum() {
let result = hash_with_salt(
Password::from("the minimum number is still observed"),
b"$6$rounds=10$roundstoolow",
);
assert_eq!(
result,
"$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."
);
}
#[test]
fn test_verify_correct_password() {
let h = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
assert_eq!(verify(Password::from("Hello world!"), h), Ok(true));
}
#[test]
fn test_verify_wrong_password() {
let h = "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1";
assert_eq!(verify(Password::from("wrong password"), h), Ok(false));
}
#[test]
fn test_verify_with_rounds() {
let h = "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.";
assert_eq!(verify(Password::from("Hello world!"), h), Ok(true));
assert_eq!(verify(Password::from("Hello world"), h), Ok(false)); }
#[test]
fn test_verify_invalid_hash_format() {
assert_eq!(
verify(Password::from("password"), "invalid_hash"),
Err(InvalidHash)
);
assert_eq!(
verify(Password::from("password"), "$5$saltstring$hash"),
Err(InvalidHash)
); assert_eq!(
verify(Password::from("password"), "$6$nosalt"),
Err(InvalidHash)
); }
#[test]
fn test_invalid_hash_display() {
let s = format!("{}", InvalidHash);
assert!(s.contains("$6$"));
}
#[test]
fn test_constant_time_comparison_smoke() {
let h1 = hash_with_salt(Password::from("password1"), b"$6$salt");
let h2 = hash_with_salt(Password::from("password2"), b"$6$salt");
assert_eq!(verify(Password::from("password1"), &h2), Ok(false));
assert_eq!(verify(Password::from("password2"), &h1), Ok(false));
}
#[test]
fn test_hash_default_rounds() {
let h = hash(Password::from("test_password"), None);
assert!(h.starts_with("$6$"));
assert!(!h.contains("rounds="));
assert_eq!(verify(Password::from("test_password"), &h), Ok(true));
assert_eq!(verify(Password::from("wrong_password"), &h), Ok(false));
}
#[test]
fn test_hash_custom_rounds() {
let h = hash(Password::from("test_password"), Some(10000));
assert!(h.starts_with("$6$rounds=10000$"));
assert_eq!(verify(Password::from("test_password"), &h), Ok(true));
assert_eq!(verify(Password::from("wrong_password"), &h), Ok(false));
}
#[test]
fn test_hash_different_salts() {
let h1 = hash(Password::from("test_password"), None);
let h2 = hash(Password::from("test_password"), None);
assert_ne!(h1, h2);
assert_eq!(verify(Password::from("test_password"), &h1), Ok(true));
assert_eq!(verify(Password::from("test_password"), &h2), Ok(true));
}
#[test]
fn test_hash_salt_length_and_alphabet() {
let h = hash(Password::from("test"), None);
let parts: Vec<&str> = h.splitn(4, '$').collect();
assert_eq!(parts.len(), 4);
assert_eq!(parts[1], "6");
let salt = parts[2];
assert_eq!(salt.len(), SALT_LEN_MAX);
for c in salt.chars() {
assert!(B64_CHARS.contains(&(c as u8)));
}
}
#[test]
fn test_hash_rounds_clamping() {
let h_low = hash(Password::from("test"), Some(100));
assert!(h_low.contains(&format!("rounds={}$", ROUNDS_MIN)));
let h_min = hash(Password::from("test"), Some(ROUNDS_MIN));
assert!(h_min.contains(&format!("rounds={}$", ROUNDS_MIN)));
let h_normal = hash(Password::from("test"), Some(10000));
assert!(h_normal.contains("rounds=10000$"));
assert_eq!(verify(Password::from("test"), &h_low), Ok(true));
assert_eq!(verify(Password::from("test"), &h_min), Ok(true));
assert_eq!(verify(Password::from("test"), &h_normal), Ok(true));
}
#[test]
fn test_hash_some_default_omits_rounds_segment() {
let h = hash(Password::from("x"), Some(ROUNDS_DEFAULT));
assert!(!h.contains("rounds="));
}
#[test]
fn test_hash_empty_password() {
let h = hash(Password::from(""), None);
assert_eq!(verify(Password::from(""), &h), Ok(true));
assert_eq!(verify(Password::from("not_empty"), &h), Ok(false));
}
#[test]
fn test_hash_unicode() {
let pw = "пароль🔐test";
let h = hash(Password::from(pw), Some(5000));
assert_eq!(verify(Password::from(pw), &h), Ok(true));
assert_eq!(verify(Password::from("wrong"), &h), Ok(false));
}
#[test]
fn test_salt_spec_output_size() {
let salt = "saltstring";
assert_eq!(salt_spec_output_size(salt.len(), false), 14);
assert_eq!(salt_spec_output_size(salt.len(), true), 31);
assert_eq!(salt_spec_output_size(SALT_LEN_MAX, true), 37);
}
#[test]
fn test_secure_zero_bytes() {
let mut v = alloc::vec![0x42u8; 128];
crypto::secure_zero_bytes(&mut v);
assert!(v.iter().all(|&b| b == 0));
}
#[test]
fn test_password_drop_zeros_buffer() {
let p = Password::from("secret");
drop(p);
let bytes = alloc::vec![0xAAu8; 32];
let p = Password::from_bytes(bytes);
let recovered = p.into_bytes();
assert!(recovered.iter().all(|&b| b == 0xAA));
}
#[test]
fn test_password_debug_does_not_leak() {
let p = Password::from("super-secret-value");
let dbg = format!("{:?}", p);
assert!(!dbg.contains("super-secret-value"));
assert!(dbg.contains("Password"));
}
}