renc 0.1.2

Rust Encryption Engine compatible with the zenc file format
Documentation
use argon2::{Algorithm, Argon2, Params, Version};
use crate::RencError;

/// Argon2id parameter set stored in renc headers.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KdfParams {
    pub mem_kib: u32,
    pub iterations: u32,
    pub parallelism: u32,
}

impl KdfParams {
    /// Default parameters for password mode.
    pub fn password_default() -> Self {
        Self {
            mem_kib: 65_536,
            iterations: 3,
            parallelism: 4,
        }
    }

    /// Default parameters for public-key mode.
    pub fn pubkey_default() -> Self {
        Self {
            mem_kib: 1_024,
            iterations: 1,
            parallelism: 1,
        }
    }

    /// Serialize to 12 bytes in little-endian (mem, iters, parallelism).
    pub fn to_bytes(self) -> [u8; 12] {
        let mut out = [0u8; 12];
        out[..4].copy_from_slice(&self.mem_kib.to_le_bytes());
        out[4..8].copy_from_slice(&self.iterations.to_le_bytes());
        out[8..12].copy_from_slice(&self.parallelism.to_le_bytes());
        out
    }

    /// Deserialize from 12 bytes in little-endian.
    pub fn from_bytes(bytes: [u8; 12]) -> Self {
        let mem_kib = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
        let iterations = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
        let parallelism = u32::from_le_bytes(bytes[8..12].try_into().unwrap());
        Self {
            mem_kib,
            iterations,
            parallelism,
        }
    }
}

/// Derive a 32-byte key using Argon2id.
pub fn derive_key(input: &[u8], salt: &[u8; 16], params: KdfParams) -> Result<[u8; 32], RencError> {
    let argon_params = Params::new(
        params.mem_kib,
        params.iterations,
        params.parallelism,
        Some(32),
    )
    .map_err(|err| RencError::Crypto(format!("Invalid KDF params: {err}")))?;
    let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon_params);
    let mut output = [0u8; 32];
    argon2
        .hash_password_into(input, salt, &mut output)
        .map_err(|err| RencError::Crypto(format!("KDF failed: {err}")))?;

    Ok(output)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn kdf_round_trip() {
        let params = KdfParams {
            mem_kib: 8,
            iterations: 1,
            parallelism: 1,
        };
        let salt = [1u8; 16];
        let key1 = derive_key(b"password", &salt, params).expect("kdf");
        let key2 = derive_key(b"password", &salt, params).expect("kdf");
        assert_eq!(key1, key2);
    }
}