use std::fmt;
use argon2::{Algorithm, Argon2, Params, Version};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::image_io::phash::PHashSalt;
const M_COST: u32 = 131_072;
const T_COST: u32 = 4;
const PARALLELISM: u32 = 2;
const MASTER_KEY_LEN: usize = 32;
#[derive(Debug)]
pub enum KdfError {
Argon2Error(String),
EmptyPassword,
}
impl fmt::Display for KdfError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KdfError::Argon2Error(message) => {
write!(f, "argon2id key derivation failed: {message}")
}
KdfError::EmptyPassword => write!(f, "the password must not be empty"),
}
}
}
impl std::error::Error for KdfError {}
#[derive(ZeroizeOnDrop)]
pub struct MasterKey([u8; MASTER_KEY_LEN]);
impl MasterKey {
pub(crate) fn new(bytes: [u8; MASTER_KEY_LEN]) -> Self {
Self(bytes)
}
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.0
}
}
pub trait KeyDeriver: Send + Sync {
fn derive_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<MasterKey, KdfError>;
fn derive(&self, password: &[u8], salt: &PHashSalt) -> Result<MasterKey, KdfError> {
self.derive_with_salt(password, salt.as_bytes())
}
}
pub struct Argon2Kdf {
m_cost: u32,
t_cost: u32,
parallelism: u32,
}
impl Argon2Kdf {
pub fn default_secure() -> Self {
Self {
m_cost: M_COST,
t_cost: T_COST,
parallelism: PARALLELISM,
}
}
#[cfg(any(test, feature = "test-utils"))]
pub fn low_cost_for_tests() -> Self {
Self {
m_cost: 8,
t_cost: 1,
parallelism: 1,
}
}
}
impl KeyDeriver for Argon2Kdf {
fn derive_with_salt(&self, password: &[u8], salt: &[u8]) -> Result<MasterKey, KdfError> {
if password.is_empty() {
return Err(KdfError::EmptyPassword);
}
let params = Params::new(self.m_cost, self.t_cost, self.parallelism, None)
.map_err(|err| KdfError::Argon2Error(err.to_string()))?;
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
let mut bytes = [0u8; MASTER_KEY_LEN];
let outcome = argon2
.hash_password_into(password, salt, &mut bytes)
.map_err(|err| KdfError::Argon2Error(err.to_string()));
let result = outcome.map(|()| MasterKey::new(bytes));
bytes.zeroize();
result
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
use super::*;
fn salt(fill: u8) -> PHashSalt {
PHashSalt::new([fill; 32])
}
#[test]
fn derivation_is_deterministic_in_both_its_inputs() {
let kdf = Argon2Kdf::low_cost_for_tests();
let first = kdf.derive(b"passphrase", &salt(1)).expect("must derive");
let again = kdf.derive(b"passphrase", &salt(1)).expect("must derive");
let other_password = kdf.derive(b"passphrasf", &salt(1)).expect("must derive");
let other_salt = kdf.derive(b"passphrase", &salt(2)).expect("must derive");
assert_eq!(first.as_bytes(), again.as_bytes());
assert_ne!(first.as_bytes(), other_password.as_bytes());
assert_ne!(first.as_bytes(), other_salt.as_bytes());
assert_eq!(first.as_bytes().len(), MASTER_KEY_LEN);
}
#[test]
fn the_container_salt_is_the_general_salt() {
let kdf = Argon2Kdf::low_cost_for_tests();
let through_hash = kdf.derive(b"passphrase", &salt(3)).expect("must derive");
let through_slice = kdf
.derive_with_salt(b"passphrase", &[3u8; 32])
.expect("must derive");
let other_salt = kdf
.derive_with_salt(b"passphrase", &[4u8; 32])
.expect("must derive");
assert_eq!(through_hash.as_bytes(), through_slice.as_bytes());
assert_ne!(through_hash.as_bytes(), other_salt.as_bytes());
}
#[test]
fn an_empty_password_is_refused() {
let error = Argon2Kdf::low_cost_for_tests()
.derive(&[], &salt(1))
.map(|_| ())
.expect_err("an empty password must never be honoured");
assert!(matches!(error, KdfError::EmptyPassword), "got: {error:?}");
}
#[test]
fn parameters_argon2_refuses_are_reported() {
let broken = Argon2Kdf {
m_cost: 0,
t_cost: 0,
parallelism: 0,
};
let error = broken
.derive(b"passphrase", &salt(1))
.map(|_| ())
.expect_err("a memory cost of zero must be refused");
assert!(matches!(error, KdfError::Argon2Error(_)), "got: {error:?}");
}
#[test]
fn the_default_deriver_carries_the_compiled_in_cost() {
let kdf = Argon2Kdf::default_secure();
assert_eq!(
(kdf.m_cost, kdf.t_cost, kdf.parallelism),
(M_COST, T_COST, PARALLELISM)
);
}
#[test]
fn every_failure_explains_itself() {
assert!(KdfError::EmptyPassword.to_string().contains("empty"));
assert!(KdfError::Argon2Error("bad params".to_owned())
.to_string()
.contains("bad params"));
}
}