#![allow(clippy::indexing_slicing)]
#![allow(clippy::unwrap_used)]
use alloc::vec::Vec;
use core::fmt;
use super::pbkdf2::Pbkdf2Sha256;
use crate::traits::{VerificationError, ct};
pub const BLOCK_SIZE: usize = 64;
const BLOCK_WORDS: usize = BLOCK_SIZE / 4;
const DEFAULT_LOG_N: u8 = 17;
const DEFAULT_R: u32 = 8;
const DEFAULT_P: u32 = 1;
const DEFAULT_OUTPUT_LEN: u32 = 32;
pub const MIN_SALT_LEN: usize = 16;
pub const MIN_OUTPUT_LEN: usize = 1;
const MAX_R_TIMES_P: u64 = (1u64 << 30) - 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScryptError {
InvalidLogN,
InvalidR,
InvalidP,
InvalidOutputLen,
ResourceOverflow,
AllocationFailed,
EntropyUnavailable,
}
impl fmt::Display for ScryptError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidLogN => "scrypt log_n must be in 1..=63",
Self::InvalidR => "scrypt r must be at least 1",
Self::InvalidP => "scrypt p must be at least 1 and satisfy r * p <= 2^30 - 1",
Self::InvalidOutputLen => "scrypt output length must be at least 1",
Self::ResourceOverflow => "scrypt parameters exceed the target's address space",
Self::AllocationFailed => "scrypt working-set allocation failed",
Self::EntropyUnavailable => "scrypt entropy source unavailable",
})
}
}
impl core::error::Error for ScryptError {}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ScryptParams {
log_n: u8,
r: u32,
p: u32,
output_len: u32,
}
impl fmt::Debug for ScryptParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ScryptParams")
.field("log_n", &self.log_n)
.field("r", &self.r)
.field("p", &self.p)
.field("output_len", &self.output_len)
.finish()
}
}
impl Default for ScryptParams {
fn default() -> Self {
Self::new()
}
}
impl ScryptParams {
#[must_use]
pub const fn new() -> Self {
Self {
log_n: DEFAULT_LOG_N,
r: DEFAULT_R,
p: DEFAULT_P,
output_len: DEFAULT_OUTPUT_LEN,
}
}
#[must_use]
pub const fn log_n(mut self, lg_n: u8) -> Self {
self.log_n = lg_n;
self
}
#[must_use]
pub const fn r(mut self, r: u32) -> Self {
self.r = r;
self
}
#[must_use]
pub const fn p(mut self, p: u32) -> Self {
self.p = p;
self
}
#[must_use]
pub const fn output_len(mut self, t: u32) -> Self {
self.output_len = t;
self
}
pub const fn build(self) -> Result<Self, ScryptError> {
match self.validate() {
Ok(()) => Ok(self),
Err(e) => Err(e),
}
}
pub const fn validate(&self) -> Result<(), ScryptError> {
if self.log_n < 1 || self.log_n > 63 {
return Err(ScryptError::InvalidLogN);
}
if self.r < 1 {
return Err(ScryptError::InvalidR);
}
if self.p < 1 {
return Err(ScryptError::InvalidP);
}
let rp = (self.r as u64) * (self.p as u64);
if rp > MAX_R_TIMES_P {
return Err(ScryptError::InvalidP);
}
if (self.output_len as usize) < MIN_OUTPUT_LEN {
return Err(ScryptError::InvalidOutputLen);
}
Ok(())
}
#[must_use]
pub const fn get_log_n(&self) -> u8 {
self.log_n
}
#[must_use]
pub const fn get_r(&self) -> u32 {
self.r
}
#[must_use]
pub const fn get_p(&self) -> u32 {
self.p
}
#[must_use]
pub const fn get_output_len(&self) -> u32 {
self.output_len
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ScryptVerifyPolicy {
pub max_log_n: u8,
pub max_r: u32,
pub max_p: u32,
pub max_output_len: usize,
}
impl ScryptVerifyPolicy {
#[must_use]
pub const fn new(max_log_n: u8, max_r: u32, max_p: u32, max_output_len: usize) -> Self {
Self {
max_log_n,
max_r,
max_p,
max_output_len,
}
}
#[must_use]
pub const fn allows(&self, params: &ScryptParams, output_len: usize) -> bool {
params.log_n <= self.max_log_n
&& params.r <= self.max_r
&& params.p <= self.max_p
&& output_len <= self.max_output_len
}
}
impl Default for ScryptVerifyPolicy {
fn default() -> Self {
Self::new(DEFAULT_LOG_N, DEFAULT_R, DEFAULT_P, DEFAULT_OUTPUT_LEN as usize)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum KernelId {
Portable,
}
impl KernelId {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Portable => "portable",
}
}
}
pub const ALL_KERNELS: &[KernelId] = &[KernelId::Portable];
#[must_use]
pub const fn required_caps(kernel: KernelId) -> crate::platform::Caps {
match kernel {
KernelId::Portable => crate::platform::Caps::from_words([0; 4]),
}
}
#[inline]
#[allow(dead_code)] fn active_kernel() -> KernelId {
KernelId::Portable
}
#[repr(align(16))]
#[derive(Clone, Copy)]
struct SalsaBlock([u32; BLOCK_WORDS]);
impl SalsaBlock {
#[inline(always)]
const fn zero() -> Self {
Self([0u32; BLOCK_WORDS])
}
}
#[inline(always)]
#[allow(clippy::too_many_lines)]
fn salsa20_8(block: &mut SalsaBlock) {
let input = block.0;
let mut y = block.0;
let mut round = 0u32;
while round < 4 {
y[4] ^= y[0].wrapping_add(y[12]).rotate_left(7);
y[8] ^= y[4].wrapping_add(y[0]).rotate_left(9);
y[12] ^= y[8].wrapping_add(y[4]).rotate_left(13);
y[0] ^= y[12].wrapping_add(y[8]).rotate_left(18);
y[9] ^= y[5].wrapping_add(y[1]).rotate_left(7);
y[13] ^= y[9].wrapping_add(y[5]).rotate_left(9);
y[1] ^= y[13].wrapping_add(y[9]).rotate_left(13);
y[5] ^= y[1].wrapping_add(y[13]).rotate_left(18);
y[14] ^= y[10].wrapping_add(y[6]).rotate_left(7);
y[2] ^= y[14].wrapping_add(y[10]).rotate_left(9);
y[6] ^= y[2].wrapping_add(y[14]).rotate_left(13);
y[10] ^= y[6].wrapping_add(y[2]).rotate_left(18);
y[3] ^= y[15].wrapping_add(y[11]).rotate_left(7);
y[7] ^= y[3].wrapping_add(y[15]).rotate_left(9);
y[11] ^= y[7].wrapping_add(y[3]).rotate_left(13);
y[15] ^= y[11].wrapping_add(y[7]).rotate_left(18);
y[1] ^= y[0].wrapping_add(y[3]).rotate_left(7);
y[2] ^= y[1].wrapping_add(y[0]).rotate_left(9);
y[3] ^= y[2].wrapping_add(y[1]).rotate_left(13);
y[0] ^= y[3].wrapping_add(y[2]).rotate_left(18);
y[6] ^= y[5].wrapping_add(y[4]).rotate_left(7);
y[7] ^= y[6].wrapping_add(y[5]).rotate_left(9);
y[4] ^= y[7].wrapping_add(y[6]).rotate_left(13);
y[5] ^= y[4].wrapping_add(y[7]).rotate_left(18);
y[11] ^= y[10].wrapping_add(y[9]).rotate_left(7);
y[8] ^= y[11].wrapping_add(y[10]).rotate_left(9);
y[9] ^= y[8].wrapping_add(y[11]).rotate_left(13);
y[10] ^= y[9].wrapping_add(y[8]).rotate_left(18);
y[12] ^= y[15].wrapping_add(y[14]).rotate_left(7);
y[13] ^= y[12].wrapping_add(y[15]).rotate_left(9);
y[14] ^= y[13].wrapping_add(y[12]).rotate_left(13);
y[15] ^= y[14].wrapping_add(y[13]).rotate_left(18);
round = round.strict_add(1);
}
let mut i = 0usize;
while i < BLOCK_WORDS {
block.0[i] = input[i].wrapping_add(y[i]);
i = i.strict_add(1);
}
}
#[inline(always)]
fn xor_block_into(dst: &mut SalsaBlock, src: &SalsaBlock) {
for (d, s) in dst.0.iter_mut().zip(src.0.iter()) {
*d ^= *s;
}
}
#[inline]
fn block_mix_into(src: &[SalsaBlock], dst: &mut [SalsaBlock], r: usize) {
let two_r = r.strict_mul(2);
debug_assert_eq!(src.len(), two_r);
debug_assert_eq!(dst.len(), two_r);
let mut x = src[two_r.strict_sub(1)];
for (i, block_in) in src.iter().enumerate() {
xor_block_into(&mut x, block_in);
salsa20_8(&mut x);
let out = if i & 1 == 0 { i >> 1 } else { r.strict_add(i >> 1) };
dst[out] = x;
}
}
#[inline(always)]
fn integerify_low64(block: &SalsaBlock) -> u64 {
(block.0[0] as u64) | ((block.0[1] as u64) << 32)
}
fn ro_mix(chunk: &mut [SalsaBlock], v: &mut [SalsaBlock], scratch: &mut [SalsaBlock], n: usize, r: usize) {
let two_r = r.strict_mul(2);
debug_assert_eq!(chunk.len(), two_r);
debug_assert_eq!(v.len(), n.strict_mul(two_r));
debug_assert_eq!(scratch.len(), two_r);
debug_assert_eq!(n & 1, 0, "n must be even (log_n ≥ 1)");
let pairs = n >> 1;
let mut v_off = 0usize;
for _ in 0..pairs {
v[v_off..v_off.strict_add(two_r)].copy_from_slice(chunk);
block_mix_into(chunk, scratch, r);
v_off = v_off.strict_add(two_r);
v[v_off..v_off.strict_add(two_r)].copy_from_slice(scratch);
block_mix_into(scratch, chunk, r);
v_off = v_off.strict_add(two_r);
}
let n_mask = (n as u64).wrapping_sub(1);
for _ in 0..pairs {
let j = (integerify_low64(&chunk[two_r.strict_sub(1)]) & n_mask) as usize;
let v_off = j.strict_mul(two_r);
for k in 0..two_r {
xor_block_into(&mut chunk[k], &v[v_off.strict_add(k)]);
}
block_mix_into(chunk, scratch, r);
let j = (integerify_low64(&scratch[two_r.strict_sub(1)]) & n_mask) as usize;
let v_off = j.strict_mul(two_r);
for k in 0..two_r {
xor_block_into(&mut scratch[k], &v[v_off.strict_add(k)]);
}
block_mix_into(scratch, chunk, r);
}
}
#[inline]
fn zeroize_u32_slice_no_fence(words: &mut [u32]) {
let mut chunks = words.chunks_exact_mut(16);
for chunk in &mut chunks {
unsafe { core::ptr::write_volatile(chunk.as_mut_ptr().cast::<[u32; 16]>(), [0u32; 16]) };
}
for w in chunks.into_remainder() {
unsafe { core::ptr::write_volatile(w, 0) };
}
}
#[inline]
fn zeroize_blocks_no_fence(blocks: &mut [SalsaBlock]) {
for block in blocks {
zeroize_u32_slice_no_fence(&mut block.0);
}
}
struct ScryptState {
b_bytes: Vec<u8>,
b_u32: Vec<SalsaBlock>,
v: Vec<SalsaBlock>,
scratch: Vec<SalsaBlock>,
}
impl ScryptState {
fn new(total_b_blocks: usize, v_blocks: usize, scratch_blocks: usize) -> Result<Self, ScryptError> {
let b_bytes_len = total_b_blocks
.checked_mul(BLOCK_SIZE)
.ok_or(ScryptError::ResourceOverflow)?;
let b_bytes = alloc_u8_vec(b_bytes_len)?;
let b_u32 = alloc_block_vec(total_b_blocks)?;
let v = alloc_block_vec(v_blocks)?;
let scratch = alloc_block_vec(scratch_blocks)?;
Ok(Self {
b_bytes,
b_u32,
v,
scratch,
})
}
}
impl Drop for ScryptState {
fn drop(&mut self) {
ct::zeroize_no_fence(&mut self.b_bytes);
zeroize_blocks_no_fence(&mut self.b_u32);
zeroize_blocks_no_fence(&mut self.v);
zeroize_blocks_no_fence(&mut self.scratch);
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}
}
fn alloc_u8_vec(len: usize) -> Result<Vec<u8>, ScryptError> {
let mut v: Vec<u8> = Vec::new();
v.try_reserve_exact(len).map_err(|_| ScryptError::AllocationFailed)?;
v.resize(len, 0);
Ok(v)
}
fn alloc_block_vec(len: usize) -> Result<Vec<SalsaBlock>, ScryptError> {
let mut v: Vec<SalsaBlock> = Vec::new();
v.try_reserve_exact(len).map_err(|_| ScryptError::AllocationFailed)?;
v.resize(len, SalsaBlock::zero());
Ok(v)
}
fn scrypt_hash(params: &ScryptParams, password: &[u8], salt: &[u8], out: &mut [u8]) -> Result<(), ScryptError> {
params.validate()?;
if out.len() != params.output_len as usize {
return Err(ScryptError::InvalidOutputLen);
}
let log_n = params.log_n as u32;
if log_n >= (usize::BITS) {
return Err(ScryptError::ResourceOverflow);
}
let n: usize = 1usize.checked_shl(log_n).ok_or(ScryptError::ResourceOverflow)?;
let r: usize = params.r as usize;
let p: usize = params.p as usize;
let two_r = r.checked_mul(2).ok_or(ScryptError::ResourceOverflow)?;
let total_b_blocks = p.checked_mul(two_r).ok_or(ScryptError::ResourceOverflow)?;
let v_blocks = n.checked_mul(two_r).ok_or(ScryptError::ResourceOverflow)?;
let mut state = ScryptState::new(total_b_blocks, v_blocks, two_r)?;
let prf = Pbkdf2Sha256::new(password);
prf
.derive(salt, 1, &mut state.b_bytes)
.map_err(|_| ScryptError::InvalidOutputLen)?;
for (block, chunk) in state.b_u32.iter_mut().zip(state.b_bytes.chunks_exact(BLOCK_SIZE)) {
for (word, bytes) in block.0.iter_mut().zip(chunk.chunks_exact(4)) {
let arr: [u8; 4] = bytes.try_into().unwrap();
*word = u32::from_le_bytes(arr);
}
}
for chunk_idx in 0..p {
let chunk_start = chunk_idx.strict_mul(two_r);
let chunk_end = chunk_start.strict_add(two_r);
let chunk = &mut state.b_u32[chunk_start..chunk_end];
ro_mix(chunk, &mut state.v, &mut state.scratch, n, r);
}
for (block, chunk) in state.b_u32.iter().zip(state.b_bytes.chunks_exact_mut(BLOCK_SIZE)) {
for (word, bytes) in block.0.iter().zip(chunk.chunks_exact_mut(4)) {
bytes.copy_from_slice(&word.to_le_bytes());
}
}
prf
.derive(&state.b_bytes, 1, out)
.map_err(|_| ScryptError::InvalidOutputLen)?;
Ok(())
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Scrypt;
impl Scrypt {
pub const ALGORITHM: &'static str = "scrypt";
pub const MIN_SALT_LEN: usize = MIN_SALT_LEN;
pub const MIN_OUTPUT_LEN: usize = MIN_OUTPUT_LEN;
pub fn hash(params: &ScryptParams, password: &[u8], salt: &[u8], out: &mut [u8]) -> Result<(), ScryptError> {
scrypt_hash(params, password, salt, out)
}
pub fn hash_array<const N: usize>(
params: &ScryptParams,
password: &[u8],
salt: &[u8],
) -> Result<[u8; N], ScryptError> {
let mut out = [0u8; N];
Self::hash(params, password, salt, &mut out)?;
Ok(out)
}
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify(params: &ScryptParams, password: &[u8], salt: &[u8], expected: &[u8]) -> Result<(), VerificationError> {
let actual_len = params.output_len as usize;
let mut actual = alloc::vec![0u8; actual_len];
let hash_failed = Self::hash(params, password, salt, &mut actual).is_err();
let bytes_match = ct::constant_time_eq(&actual, expected);
ct::zeroize(&mut actual);
let success = !hash_failed & bytes_match;
if core::hint::black_box(success) {
Ok(())
} else {
Err(VerificationError::new())
}
}
#[cfg(feature = "phc-strings")]
pub fn hash_string_with_salt(
params: &ScryptParams,
password: &[u8],
salt: &[u8],
) -> Result<alloc::string::String, ScryptError> {
let mut hash = alloc::vec![0u8; params.output_len as usize];
Self::hash(params, password, salt, &mut hash)?;
let encoded = phc_integration::encode_string(params, salt, &hash);
ct::zeroize(&mut hash);
Ok(encoded)
}
#[cfg(all(feature = "phc-strings", feature = "getrandom"))]
pub fn hash_string(params: &ScryptParams, password: &[u8]) -> Result<alloc::string::String, ScryptError> {
let mut salt = [0u8; 16];
getrandom::fill(&mut salt).map_err(|_| ScryptError::EntropyUnavailable)?;
Self::hash_string_with_salt(params, password, &salt)
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string(password: &[u8], encoded: &str) -> Result<(), VerificationError> {
Self::verify_string_with_policy(
password,
encoded,
&ScryptVerifyPolicy::new(u8::MAX, u32::MAX, u32::MAX, usize::MAX),
)
}
#[cfg(feature = "phc-strings")]
#[must_use = "password verification must be checked; a dropped Result silently accepts the wrong password"]
pub fn verify_string_with_policy(
password: &[u8],
encoded: &str,
policy: &ScryptVerifyPolicy,
) -> Result<(), VerificationError> {
let parsed = phc_integration::decode_string(encoded).map_err(|_| VerificationError::new())?;
if !policy.allows(&parsed.params, parsed.hash.len()) {
return Err(VerificationError::new());
}
let mut actual = alloc::vec![0u8; parsed.hash.len()];
if Self::hash(&parsed.params, password, &parsed.salt, &mut actual).is_err() {
ct::zeroize(&mut actual);
return Err(VerificationError::new());
}
let ok = ct::constant_time_eq(&actual, &parsed.hash);
ct::zeroize(&mut actual);
if core::hint::black_box(ok) {
Ok(())
} else {
Err(VerificationError::new())
}
}
#[cfg(feature = "phc-strings")]
pub fn decode_string(
encoded: &str,
) -> Result<(ScryptParams, alloc::vec::Vec<u8>, alloc::vec::Vec<u8>), crate::auth::phc::PhcError> {
let parsed = phc_integration::decode_string(encoded)?;
Ok((parsed.params, parsed.salt, parsed.hash))
}
}
#[cfg(feature = "phc-strings")]
mod phc_integration {
use alloc::{string::String, vec::Vec};
use super::{MIN_OUTPUT_LEN, ScryptParams};
use crate::auth::phc::{self, PhcError};
pub(super) struct ParsedPhc {
pub params: ScryptParams,
pub salt: Vec<u8>,
pub hash: Vec<u8>,
}
pub(super) fn encode_string(params: &ScryptParams, salt: &[u8], hash: &[u8]) -> String {
let mut out = String::new();
out.push('$');
out.push_str(super::Scrypt::ALGORITHM);
out.push_str("$ln=");
phc::push_u32_decimal(&mut out, u32::from(params.get_log_n()));
out.push_str(",r=");
phc::push_u32_decimal(&mut out, params.get_r());
out.push_str(",p=");
phc::push_u32_decimal(&mut out, params.get_p());
out.push('$');
phc::base64_encode_into(salt, &mut out);
out.push('$');
phc::base64_encode_into(hash, &mut out);
out
}
pub(super) fn decode_string(encoded: &str) -> Result<ParsedPhc, PhcError> {
let parts = phc::parse(encoded)?;
if parts.algorithm != super::Scrypt::ALGORITHM {
return Err(PhcError::AlgorithmMismatch);
}
if parts.version.is_some() {
return Err(PhcError::UnsupportedVersion);
}
let mut log_n: Option<u32> = None;
let mut r: Option<u32> = None;
let mut p: Option<u32> = None;
for pair in phc::PhcParamIter::new(parts.parameters) {
let (k, v) = pair?;
let value = phc::parse_param_u32(v)?;
match k {
"ln" => {
if log_n.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
"r" => {
if r.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
"p" => {
if p.replace(value).is_some() {
return Err(PhcError::DuplicateParam);
}
}
_ => return Err(PhcError::UnknownParam),
}
}
let log_n_u32 = log_n.ok_or(PhcError::MissingParam)?;
let r = r.ok_or(PhcError::MissingParam)?;
let p = p.ok_or(PhcError::MissingParam)?;
if log_n_u32 > u8::MAX as u32 {
return Err(PhcError::ParamOutOfRange);
}
let salt = phc::decode_base64_to_vec(parts.salt_b64)?;
let hash = phc::decode_base64_to_vec(parts.hash_b64)?;
if hash.len() < MIN_OUTPUT_LEN {
return Err(PhcError::InvalidLength);
}
let params = ScryptParams::new()
.log_n(log_n_u32 as u8)
.r(r)
.p(p)
.output_len(hash.len() as u32)
.build()
.map_err(|_| PhcError::ParamOutOfRange)?;
Ok(ParsedPhc { params, salt, hash })
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
const RFC_V1_EXPECTED: [u8; 64] = [
0x77, 0xd6, 0x57, 0x62, 0x38, 0x65, 0x7b, 0x20, 0x3b, 0x19, 0xca, 0x42, 0xc1, 0x8a, 0x04, 0x97, 0xf1, 0x6b, 0x48,
0x44, 0xe3, 0x07, 0x4a, 0xe8, 0xdf, 0xdf, 0xfa, 0x3f, 0xed, 0xe2, 0x14, 0x42, 0xfc, 0xd0, 0x06, 0x9d, 0xed, 0x09,
0x48, 0xf8, 0x32, 0x6a, 0x75, 0x3a, 0x0f, 0xc8, 0x1f, 0x17, 0xe8, 0xd3, 0xe0, 0xfb, 0x2e, 0x0d, 0x36, 0x28, 0xcf,
0x35, 0xe2, 0x0c, 0x38, 0xd1, 0x89, 0x06,
];
#[test]
fn rfc7914_vector_1_empty_inputs() {
let params = ScryptParams::new()
.log_n(4) .r(1)
.p(1)
.output_len(64)
.build()
.unwrap();
let mut out = [0u8; 64];
Scrypt::hash(¶ms, b"", b"", &mut out).unwrap();
assert_eq!(out, RFC_V1_EXPECTED);
}
fn oracle_scrypt(password: &[u8], salt: &[u8], log_n: u8, r: u32, p: u32, out_len: usize) -> alloc::vec::Vec<u8> {
let params = scrypt::Params::new(log_n, r, p).unwrap();
let mut out = vec![0u8; out_len];
scrypt::scrypt(password, salt, ¶ms, &mut out).unwrap();
out
}
#[test]
fn matches_oracle_small_params() {
let cases: &[(u8, u32, u32, usize)] = &[(4, 1, 1, 32), (5, 2, 1, 32), (6, 2, 2, 32)];
for &(log_n, r, p, out_len) in cases {
let params = ScryptParams::new()
.log_n(log_n)
.r(r)
.p(p)
.output_len(out_len as u32)
.build()
.unwrap();
let mut actual = vec![0u8; out_len];
Scrypt::hash(¶ms, b"password", b"salty-salty-salt", &mut actual).unwrap();
let expected = oracle_scrypt(b"password", b"salty-salty-salt", log_n, r, p, out_len);
assert_eq!(actual, expected, "mismatch log_n={log_n} r={r} p={p} T={out_len}");
}
}
#[cfg(not(miri))]
#[test]
fn matches_oracle_owasp_shape() {
let log_n = 10;
let r = 8;
let p = 1;
let out_len = 32;
let params = ScryptParams::new()
.log_n(log_n)
.r(r)
.p(p)
.output_len(out_len as u32)
.build()
.unwrap();
let password = b"correct horse battery staple";
let salt = b"random-salt-1234";
let mut actual = vec![0u8; out_len];
Scrypt::hash(¶ms, password, salt, &mut actual).unwrap();
let expected = oracle_scrypt(password, salt, log_n, r, p, out_len);
assert_eq!(actual, expected);
}
#[test]
fn verify_accepts_correct() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let h = Scrypt::hash_array::<32>(¶ms, b"password", b"random-salt-1234").unwrap();
assert!(Scrypt::verify(¶ms, b"password", b"random-salt-1234", &h).is_ok());
}
#[test]
fn verify_rejects_wrong_password() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let h = Scrypt::hash_array::<32>(¶ms, b"password", b"random-salt-1234").unwrap();
assert!(Scrypt::verify(¶ms, b"wrong-password!!", b"random-salt-1234", &h).is_err());
}
#[test]
fn verify_rejects_wrong_salt() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let h = Scrypt::hash_array::<32>(¶ms, b"password", b"random-salt-1234").unwrap();
assert!(Scrypt::verify(¶ms, b"password", b"other-salt-000000", &h).is_err());
}
#[test]
fn verify_rejects_length_mismatch() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let wrong_len = [0u8; 16];
assert!(Scrypt::verify(¶ms, b"password", b"random-salt-1234", &wrong_len).is_err());
}
#[cfg(all(feature = "phc-strings", feature = "getrandom"))]
#[test]
fn hash_string_uses_random_salt_and_verifies() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let encoded = Scrypt::hash_string(¶ms, b"password").unwrap();
assert!(Scrypt::verify_string(b"password", &encoded).is_ok());
assert!(Scrypt::verify_string(b"wrong-password", &encoded).is_err());
}
#[test]
fn verify_rejects_byte_flip_at_every_position() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let password = b"correct horse battery staple";
let salt = b"random-salt-1234";
let hash = Scrypt::hash_array::<32>(¶ms, password, salt).unwrap();
for pos in 0..hash.len() {
let mut tampered = hash;
tampered[pos] ^= 0x01;
assert!(
Scrypt::verify(¶ms, password, salt, &tampered).is_err(),
"verify must reject flip at byte {pos}",
);
}
}
#[test]
fn validate_rejects_zero_log_n() {
assert_eq!(
ScryptParams::new().log_n(0).build().unwrap_err(),
ScryptError::InvalidLogN,
);
}
#[test]
fn validate_rejects_log_n_too_large() {
assert_eq!(
ScryptParams::new().log_n(64).build().unwrap_err(),
ScryptError::InvalidLogN,
);
}
#[test]
fn validate_rejects_zero_r() {
assert_eq!(ScryptParams::new().r(0).build().unwrap_err(), ScryptError::InvalidR,);
}
#[test]
fn validate_rejects_zero_p() {
assert_eq!(ScryptParams::new().p(0).build().unwrap_err(), ScryptError::InvalidP,);
}
#[test]
fn validate_rejects_r_times_p_over_limit() {
assert_eq!(
ScryptParams::new()
.log_n(4)
.r(1 << 15)
.p(1 << 15)
.output_len(32)
.build()
.unwrap_err(),
ScryptError::InvalidP,
);
}
#[test]
fn validate_rejects_zero_output_len() {
assert_eq!(
ScryptParams::new().output_len(0).build().unwrap_err(),
ScryptError::InvalidOutputLen,
);
}
#[test]
fn output_len_mismatch_rejected() {
let params = ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap();
let mut out = [0u8; 16];
assert_eq!(
Scrypt::hash(¶ms, b"pw", b"salty-salty-salt", &mut out).unwrap_err(),
ScryptError::InvalidOutputLen,
);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn hash_rejects_impossible_memory_size_on_64bit() {
let params = ScryptParams::new()
.log_n(63)
.r(1 << 20)
.p(1)
.output_len(32)
.build()
.unwrap();
let mut out = [0u8; 32];
assert_eq!(
Scrypt::hash(¶ms, b"pw", b"salty-salty-salt", &mut out).unwrap_err(),
ScryptError::ResourceOverflow,
);
}
#[test]
fn error_is_copy_and_implements_error_trait() {
fn assert_copy<T: Copy>() {}
fn assert_err<T: core::error::Error>() {}
assert_copy::<ScryptError>();
assert_err::<ScryptError>();
}
#[test]
fn error_display_is_non_empty_for_every_variant() {
let all = [
ScryptError::InvalidLogN,
ScryptError::InvalidR,
ScryptError::InvalidP,
ScryptError::InvalidOutputLen,
ScryptError::ResourceOverflow,
ScryptError::AllocationFailed,
ScryptError::EntropyUnavailable,
];
for e in all {
let s = alloc::format!("{e}");
assert!(!s.is_empty());
}
}
#[test]
fn kernel_id_stringifies() {
assert_eq!(KernelId::Portable.as_str(), "portable");
}
#[test]
fn portable_kernel_has_no_required_caps() {
assert!(required_caps(KernelId::Portable).is_empty());
}
#[test]
fn active_kernel_is_portable() {
assert_eq!(active_kernel(), KernelId::Portable);
}
#[test]
fn salsa20_8_is_deterministic() {
let a = SalsaBlock([0x1234_5678; BLOCK_WORDS]);
let mut a1 = a;
let mut a2 = a;
salsa20_8(&mut a1);
salsa20_8(&mut a2);
assert_eq!(a1.0, a2.0);
assert_ne!(a1.0, a.0, "Salsa20/8 must not be the identity on a constant input");
}
#[cfg(feature = "phc-strings")]
mod phc_tests {
use alloc::vec;
use super::*;
use crate::auth::phc::PhcError;
fn small_params() -> ScryptParams {
ScryptParams::new().log_n(4).r(1).p(1).output_len(32).build().unwrap()
}
#[test]
fn hash_string_with_salt_round_trip() {
let params = small_params();
let salt = [0xAAu8; 16];
let encoded = Scrypt::hash_string_with_salt(¶ms, b"password", &salt).unwrap();
assert!(encoded.starts_with("$scrypt$ln=4,r=1,p=1$"));
assert!(Scrypt::verify_string(b"password", &encoded).is_ok());
assert!(Scrypt::verify_string(b"wrongpassword", &encoded).is_err());
}
#[test]
fn verify_string_with_policy_enforces_scrypt_bounds() {
let params = small_params();
let salt = [0xA1u8; 16];
let encoded = Scrypt::hash_string_with_salt(¶ms, b"password", &salt).unwrap();
let allowed = ScryptVerifyPolicy::new(4, 1, 1, 32);
assert!(Scrypt::verify_string_with_policy(b"password", &encoded, &allowed).is_ok());
let low_log_n = ScryptVerifyPolicy::new(3, 1, 1, 32);
assert!(Scrypt::verify_string_with_policy(b"password", &encoded, &low_log_n).is_err());
let short_output = ScryptVerifyPolicy::new(4, 1, 1, 31);
assert!(Scrypt::verify_string_with_policy(b"password", &encoded, &short_output).is_err());
}
#[test]
fn decode_string_extracts_params_salt_hash() {
let params = small_params();
let salt = vec![0xDDu8; 16];
let encoded = Scrypt::hash_string_with_salt(¶ms, b"pw", &salt).unwrap();
let (decoded_params, decoded_salt, decoded_hash) = Scrypt::decode_string(&encoded).unwrap();
assert_eq!(decoded_params.get_log_n(), 4);
assert_eq!(decoded_params.get_r(), 1);
assert_eq!(decoded_params.get_p(), 1);
assert_eq!(decoded_params.get_output_len(), 32);
assert_eq!(decoded_salt, salt);
assert_eq!(decoded_hash.len(), 32);
let mut rehashed = [0u8; 32];
Scrypt::hash(&decoded_params, b"pw", &decoded_salt, &mut rehashed).unwrap();
assert_eq!(rehashed.as_slice(), decoded_hash.as_slice());
}
#[test]
fn decode_string_rejects_duplicate_params() {
let params = small_params();
let encoded = Scrypt::hash_string_with_salt(¶ms, b"pw", &[0xFFu8; 16]).unwrap();
let broken = encoded.replace("r=1", "ln=4");
assert_eq!(Scrypt::decode_string(&broken).unwrap_err(), PhcError::DuplicateParam);
}
#[test]
fn decode_string_rejects_unknown_param() {
let params = small_params();
let encoded = Scrypt::hash_string_with_salt(¶ms, b"pw", &[0xFFu8; 16]).unwrap();
let broken = encoded.replace("ln=4", "bogus=1");
assert_eq!(Scrypt::decode_string(&broken).unwrap_err(), PhcError::UnknownParam);
}
#[test]
fn decode_string_rejects_algorithm_mismatch() {
assert_eq!(
Scrypt::decode_string("$argon2id$v=19$m=32,t=2,p=1$c29tZXNhbHQ$c29tZWhhc2g").unwrap_err(),
PhcError::AlgorithmMismatch,
);
}
#[test]
fn decode_string_rejects_version_segment() {
assert_eq!(
Scrypt::decode_string("$scrypt$v=1$ln=4,r=1,p=1$c29tZXNhbHQ$c29tZWhhc2g").unwrap_err(),
PhcError::UnsupportedVersion,
);
}
#[test]
fn decode_string_distinguishes_version_segment_from_version_param() {
assert_eq!(
Scrypt::decode_string("$scrypt$v=1$ln=4,r=1,p=1$c29tZXNhbHQ$c29tZWhhc2g").unwrap_err(),
PhcError::UnsupportedVersion,
);
assert_eq!(
Scrypt::decode_string("$scrypt$version=1,ln=4,r=1,p=1$c29tZXNhbHQ$c29tZWhhc2g").unwrap_err(),
PhcError::UnknownParam,
);
}
#[test]
fn decode_string_rejects_missing_required_param() {
let params = small_params();
let encoded = Scrypt::hash_string_with_salt(¶ms, b"pw", &[0x11u8; 16]).unwrap();
let broken = encoded.replace("ln=4,", "");
assert_eq!(Scrypt::decode_string(&broken).unwrap_err(), PhcError::MissingParam);
}
#[test]
fn decode_string_rejects_out_of_range_log_n() {
let params = small_params();
let encoded = Scrypt::hash_string_with_salt(¶ms, b"pw", &[0x22u8; 16]).unwrap();
let broken = encoded.replace("ln=4", "ln=999");
assert_eq!(Scrypt::decode_string(&broken).unwrap_err(), PhcError::ParamOutOfRange);
}
#[test]
fn hash_array_and_hash_agree_byte_for_byte() {
let params = small_params();
let arr = Scrypt::hash_array::<32>(¶ms, b"pw", b"salty-salty-salt").unwrap();
let mut via_hash = [0u8; 32];
Scrypt::hash(¶ms, b"pw", b"salty-salty-salt", &mut via_hash).unwrap();
assert_eq!(arr, via_hash);
}
#[test]
fn hash_is_deterministic() {
let params = small_params();
let mut a = [0u8; 32];
let mut b = [0u8; 32];
Scrypt::hash(¶ms, b"pw", b"salty-salty-salt", &mut a).unwrap();
Scrypt::hash(¶ms, b"pw", b"salty-salty-salt", &mut b).unwrap();
assert_eq!(a, b);
}
#[test]
fn encoded_format_exact_for_known_vector() {
let params = small_params();
let salt = b"exampleSALTvalue"; let encoded = Scrypt::hash_string_with_salt(¶ms, b"password", salt).unwrap();
let segments: alloc::vec::Vec<&str> = encoded.split('$').collect();
assert_eq!(segments[0], "");
assert_eq!(segments[1], "scrypt");
assert_eq!(segments[2], "ln=4,r=1,p=1");
assert_eq!(segments[3].len(), 22); assert_eq!(segments[4].len(), 43); assert_eq!(segments.len(), 5);
}
}
}