use zeroize::Zeroizing;
use crate::CryptoError;
use crate::crypto::keys::ENCRYPTION_KEY_SIZE;
use crate::error::InvalidKdfParams;
use crate::format::{read_u32_be, write_u32_be};
pub(crate) const ARGON2_SALT_SIZE: usize = 32;
pub(crate) const MAX_PASSPHRASE_LEN_BYTES: usize = 4_096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct KdfLimit {
pub max_mem_cost_kib: u32,
}
impl KdfLimit {
pub fn new(max_mem_cost_kib: u32) -> Self {
Self { max_mem_cost_kib }
}
pub fn from_mib(mib: u32) -> Result<Self, CryptoError> {
let kib = mib.checked_mul(1024).ok_or_else(|| {
CryptoError::InvalidInput(format!("KDF memory limit overflow: {} MiB", mib))
})?;
Ok(Self::new(kib))
}
}
impl Default for KdfLimit {
fn default() -> Self {
Self {
max_mem_cost_kib: KdfParams::DEFAULT_MEM_COST,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KdfParams {
pub mem_cost: u32,
pub time_cost: u32,
pub lanes: u32,
}
pub const KDF_PARAMS_SIZE: usize = 12;
const KDF_MEM_COST_OFFSET: usize = 0;
const KDF_TIME_COST_OFFSET: usize = KDF_MEM_COST_OFFSET + size_of::<u32>();
const KDF_LANES_OFFSET: usize = KDF_TIME_COST_OFFSET + size_of::<u32>();
const _: () = assert!(KDF_LANES_OFFSET + size_of::<u32>() == KDF_PARAMS_SIZE);
const ARGON2_MIN_MEM_COST_PER_LANE: u32 = 8;
impl KdfParams {
pub(crate) const DEFAULT_MEM_COST: u32 = 1_048_576; const DEFAULT_TIME_COST: u32 = 4;
const DEFAULT_LANES: u32 = 4;
pub fn to_bytes(self) -> [u8; KDF_PARAMS_SIZE] {
let mut buf = [0u8; KDF_PARAMS_SIZE];
write_u32_be(&mut buf, KDF_MEM_COST_OFFSET, self.mem_cost);
write_u32_be(&mut buf, KDF_TIME_COST_OFFSET, self.time_cost);
write_u32_be(&mut buf, KDF_LANES_OFFSET, self.lanes);
buf
}
pub(crate) const MAX_MEM_COST: u32 = 2 * 1024 * 1024; const MAX_TIME_COST: u32 = 12;
const MAX_LANES: u32 = 8;
pub(crate) fn validate_structural(&self) -> Result<(), CryptoError> {
if self.lanes == 0 || self.lanes > Self::MAX_LANES {
return Err(CryptoError::InvalidKdfParams(
InvalidKdfParams::Parallelism(self.lanes),
));
}
let min_mem_cost = ARGON2_MIN_MEM_COST_PER_LANE * self.lanes;
if self.mem_cost < min_mem_cost || self.mem_cost > Self::MAX_MEM_COST {
return Err(CryptoError::InvalidKdfParams(InvalidKdfParams::MemoryCost(
self.mem_cost,
)));
}
if self.time_cost == 0 || self.time_cost > Self::MAX_TIME_COST {
return Err(CryptoError::InvalidKdfParams(InvalidKdfParams::TimeCost(
self.time_cost,
)));
}
Ok(())
}
pub(crate) fn from_bytes_structural(
bytes: &[u8; KDF_PARAMS_SIZE],
) -> Result<Self, CryptoError> {
let params = Self {
mem_cost: read_u32_be(bytes, KDF_MEM_COST_OFFSET)?,
time_cost: read_u32_be(bytes, KDF_TIME_COST_OFFSET)?,
lanes: read_u32_be(bytes, KDF_LANES_OFFSET)?,
};
params.validate_structural()?;
Ok(params)
}
pub(crate) fn enforce_limit(self, limit: Option<&KdfLimit>) -> Result<Self, CryptoError> {
let effective_max = limit
.map(|l| l.max_mem_cost_kib)
.unwrap_or(Self::DEFAULT_MEM_COST);
if self.mem_cost > effective_max {
return Err(CryptoError::KdfResourceCapExceeded {
mem_cost_kib: self.mem_cost,
local_cap_kib: effective_max,
});
}
Ok(self)
}
pub fn from_bytes(
bytes: &[u8; KDF_PARAMS_SIZE],
limit: Option<&KdfLimit>,
) -> Result<Self, CryptoError> {
Self::from_bytes_structural(bytes)?.enforce_limit(limit)
}
pub(crate) fn validate_for_write(self, limit: Option<&KdfLimit>) -> Result<Self, CryptoError> {
self.validate_structural()?;
self.enforce_limit(limit)
}
pub fn hash_passphrase(
&self,
passphrase: &[u8],
salt: &[u8],
) -> Result<Zeroizing<[u8; ENCRYPTION_KEY_SIZE]>, CryptoError> {
self.validate_structural()?;
if passphrase.len() > MAX_PASSPHRASE_LEN_BYTES {
return Err(CryptoError::InvalidInput(format!(
"Passphrase exceeds {MAX_PASSPHRASE_LEN_BYTES}-byte structural cap"
)));
}
let params = argon2::Params::new(
self.mem_cost,
self.time_cost,
self.lanes,
Some(ENCRYPTION_KEY_SIZE),
)
.map_err(|_| {
CryptoError::InternalCryptoFailure("Internal error: Argon2id parameter rejected")
})?;
let hasher =
argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut output = Zeroizing::new([0u8; ENCRYPTION_KEY_SIZE]);
hasher
.hash_password_into(passphrase, salt, output.as_mut())
.map_err(|_| {
CryptoError::InternalCryptoFailure("Internal error: Argon2id derivation failed")
})?;
Ok(output)
}
}
impl Default for KdfParams {
fn default() -> Self {
Self {
mem_cost: Self::DEFAULT_MEM_COST,
time_cost: Self::DEFAULT_TIME_COST,
lanes: Self::DEFAULT_LANES,
}
}
}
#[cfg(test)]
impl KdfParams {
pub(crate) fn test_fast_default() -> Self {
Self {
mem_cost: ferrocrypt_test_support::TEST_FAST_KDF_MEM_COST,
time_cost: ferrocrypt_test_support::TEST_FAST_KDF_TIME_COST,
lanes: ferrocrypt_test_support::TEST_FAST_KDF_LANES,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use secrecy::SecretString;
#[test]
fn test_secret_string_creation() {
let secret = SecretString::from("my_secret_password".to_string());
let debug_str = format!("{:?}", secret);
assert!(debug_str.contains("Secret"));
}
#[test]
fn test_kdf_params_valid_defaults() {
let params = KdfParams::default();
let bytes = params.to_bytes();
assert!(KdfParams::from_bytes(&bytes, None).is_ok());
}
#[test]
fn test_kdf_params_rejects_zero_mem_cost() {
let mut bytes = KdfParams::default().to_bytes();
bytes[0..4].copy_from_slice(&0u32.to_be_bytes());
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_rejects_zero_time_cost() {
let mut bytes = KdfParams::default().to_bytes();
bytes[4..8].copy_from_slice(&0u32.to_be_bytes());
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_rejects_zero_lanes() {
let mut bytes = KdfParams::default().to_bytes();
bytes[8..12].copy_from_slice(&0u32.to_be_bytes());
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_rejects_excessive_time_cost() {
let mut bytes = KdfParams::default().to_bytes();
bytes[4..8].copy_from_slice(&13u32.to_be_bytes());
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_rejects_excessive_lanes() {
let mut bytes = KdfParams::default().to_bytes();
bytes[8..12].copy_from_slice(&9u32.to_be_bytes());
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_rejects_mem_cost_below_argon2_minimum() {
let bytes = KdfParams {
mem_cost: 31,
time_cost: 4,
lanes: 4,
}
.to_bytes();
assert!(KdfParams::from_bytes(&bytes, None).is_err());
}
#[test]
fn test_kdf_params_accepts_max_bounds() {
let bytes = KdfParams {
mem_cost: 2 * 1024 * 1024,
time_cost: 12,
lanes: 8,
}
.to_bytes();
let limit = KdfLimit::new(KdfParams::MAX_MEM_COST);
assert!(KdfParams::from_bytes(&bytes, Some(&limit)).is_ok());
}
#[test]
fn test_kdf_limit_rejects_excessive_mem_cost() {
let bytes = KdfParams {
mem_cost: 1_048_576, time_cost: 4,
lanes: 4,
}
.to_bytes();
let limit = KdfLimit::new(512 * 1024); match KdfParams::from_bytes(&bytes, Some(&limit)) {
Err(CryptoError::KdfResourceCapExceeded {
mem_cost_kib: 1_048_576,
local_cap_kib: 524_288,
}) => {}
Err(other) => panic!("expected KdfResourceCapExceeded, got: {other}"),
Ok(_) => panic!("expected KdfResourceCapExceeded error, got Ok"),
}
}
#[test]
fn test_kdf_limit_accepts_within_bound() {
let bytes = KdfParams {
mem_cost: 1_048_576, time_cost: 4,
lanes: 4,
}
.to_bytes();
let limit = KdfLimit::new(2 * 1024 * 1024); assert!(KdfParams::from_bytes(&bytes, Some(&limit)).is_ok());
}
#[test]
fn test_kdf_limit_default_accepts_default_params() {
let bytes = KdfParams::default().to_bytes();
let limit = KdfLimit::default();
assert!(KdfParams::from_bytes(&bytes, Some(&limit)).is_ok());
}
#[test]
fn test_kdf_limit_default_rejects_max_mem_cost_header() {
let bytes = KdfParams {
mem_cost: KdfParams::MAX_MEM_COST, time_cost: 4,
lanes: 4,
}
.to_bytes();
let limit = KdfLimit::default();
match KdfParams::from_bytes(&bytes, Some(&limit)) {
Err(CryptoError::KdfResourceCapExceeded {
mem_cost_kib,
local_cap_kib,
}) => {
assert_eq!(mem_cost_kib, KdfParams::MAX_MEM_COST);
assert_eq!(local_cap_kib, KdfParams::DEFAULT_MEM_COST);
}
Err(other) => panic!("expected KdfResourceCapExceeded, got: {other}"),
Ok(_) => panic!("default limit must reject a 2 GiB header"),
}
}
#[test]
fn test_kdf_limit_none_applies_default_ceiling() {
let bytes = KdfParams {
mem_cost: KdfParams::MAX_MEM_COST,
time_cost: 4,
lanes: 4,
}
.to_bytes();
match KdfParams::from_bytes(&bytes, None) {
Err(CryptoError::KdfResourceCapExceeded {
mem_cost_kib,
local_cap_kib,
}) => {
assert_eq!(mem_cost_kib, KdfParams::MAX_MEM_COST);
assert_eq!(local_cap_kib, KdfParams::DEFAULT_MEM_COST);
}
Err(other) => panic!("expected KdfResourceCapExceeded, got: {other}"),
Ok(_) => panic!("None limit must apply default ceiling"),
}
}
#[test]
fn hash_passphrase_rejects_structurally_invalid_params() {
let params = KdfParams {
mem_cost: 8,
time_cost: KdfParams::MAX_TIME_COST + 1,
lanes: 1,
};
let err = params
.hash_passphrase(b"pw", &[0u8; ARGON2_SALT_SIZE])
.unwrap_err();
assert!(
matches!(err, CryptoError::InvalidKdfParams(_)),
"expected InvalidKdfParams, got {err:?}"
);
}
#[test]
fn above_default_mem_cost_passes_structural_but_validate_for_write_none_rejects() {
let params = KdfParams {
mem_cost: KdfParams::DEFAULT_MEM_COST + 1,
time_cost: 4,
lanes: 4,
};
params
.validate_structural()
.expect("1-2 GiB band is structurally valid");
match params.validate_for_write(None) {
Err(CryptoError::KdfResourceCapExceeded { .. }) => {}
other => panic!("expected KdfResourceCapExceeded, got {other:?}"),
}
}
}