use chacha20poly1305::aead::{OsRng, rand_core::RngCore};
use secrecy::SecretString;
use zeroize::Zeroizing;
use crate::CryptoError;
use crate::crypto::hkdf::hkdf_expand_sha3_256;
use crate::crypto::kdf::{ARGON2_SALT_SIZE, KdfParams};
use crate::crypto::mac::HMAC_KEY_SIZE;
const CSPRNG_FAILURE: CryptoError =
CryptoError::InternalCryptoFailure("Internal error: CSPRNG read failed");
pub(crate) const ENCRYPTION_KEY_SIZE: usize = 32;
pub(crate) const FILE_KEY_SIZE: usize = 32;
pub(crate) const HKDF_INFO_PAYLOAD: &[u8] = b"ferrocrypt/v1/payload";
pub(crate) const HKDF_INFO_HEADER: &[u8] = b"ferrocrypt/v1/header";
pub(crate) fn random_bytes<const N: usize>() -> Result<[u8; N], CryptoError> {
let mut buf = [0u8; N];
OsRng.try_fill_bytes(&mut buf).map_err(|_| CSPRNG_FAILURE)?;
Ok(buf)
}
pub(crate) fn random_secret<const N: usize>() -> Result<Zeroizing<[u8; N]>, CryptoError> {
let mut buf = Zeroizing::new([0u8; N]);
OsRng
.try_fill_bytes(buf.as_mut())
.map_err(|_| CSPRNG_FAILURE)?;
Ok(buf)
}
pub(crate) struct FileKey(Zeroizing<[u8; FILE_KEY_SIZE]>);
impl std::fmt::Debug for FileKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FileKey(<redacted>)")
}
}
impl FileKey {
pub(crate) fn generate() -> Result<Self, CryptoError> {
Ok(Self(random_secret::<FILE_KEY_SIZE>()?))
}
pub(crate) fn from_zeroizing(bytes: Zeroizing<[u8; FILE_KEY_SIZE]>) -> Self {
Self(bytes)
}
pub(crate) fn expose(&self) -> &[u8; FILE_KEY_SIZE] {
&self.0
}
#[cfg(test)]
pub(crate) fn from_bytes_for_tests(bytes: [u8; FILE_KEY_SIZE]) -> Self {
Self::from_zeroizing(Zeroizing::new(bytes))
}
}
pub(crate) struct PayloadKey(Zeroizing<[u8; ENCRYPTION_KEY_SIZE]>);
impl std::fmt::Debug for PayloadKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PayloadKey(<redacted>)")
}
}
impl PayloadKey {
pub(crate) fn from_zeroizing(bytes: Zeroizing<[u8; ENCRYPTION_KEY_SIZE]>) -> Self {
Self(bytes)
}
pub(crate) fn expose(&self) -> &[u8; ENCRYPTION_KEY_SIZE] {
&self.0
}
#[cfg(test)]
pub(crate) fn from_bytes_for_tests(bytes: [u8; ENCRYPTION_KEY_SIZE]) -> Self {
Self::from_zeroizing(Zeroizing::new(bytes))
}
}
pub(crate) struct HeaderKey(Zeroizing<[u8; HMAC_KEY_SIZE]>);
impl std::fmt::Debug for HeaderKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("HeaderKey(<redacted>)")
}
}
impl HeaderKey {
pub(crate) fn from_zeroizing(bytes: Zeroizing<[u8; HMAC_KEY_SIZE]>) -> Self {
Self(bytes)
}
pub(crate) fn expose(&self) -> &[u8; HMAC_KEY_SIZE] {
&self.0
}
#[cfg(test)]
pub(crate) fn from_bytes_for_tests(bytes: [u8; HMAC_KEY_SIZE]) -> Self {
Self::from_zeroizing(Zeroizing::new(bytes))
}
}
pub(crate) fn derive_passphrase_wrap_key(
passphrase: &SecretString,
argon2_salt: &[u8; ARGON2_SALT_SIZE],
kdf_params: &KdfParams,
info: &[u8],
) -> Result<Zeroizing<[u8; 32]>, CryptoError> {
use secrecy::ExposeSecret;
let ikm = kdf_params.hash_passphrase(passphrase.expose_secret().as_bytes(), argon2_salt)?;
hkdf_expand_sha3_256(Some(argon2_salt), ikm.as_ref(), info)
}
pub(crate) struct DerivedSubkeys {
pub payload_key: PayloadKey,
pub header_key: HeaderKey,
}
pub(crate) fn derive_subkeys(
file_key: &FileKey,
stream_nonce: &[u8; crate::crypto::stream::STREAM_NONCE_SIZE],
) -> Result<DerivedSubkeys, CryptoError> {
let payload_bytes =
hkdf_expand_sha3_256(Some(stream_nonce), file_key.expose(), HKDF_INFO_PAYLOAD)?;
let header_bytes = hkdf_expand_sha3_256(None, file_key.expose(), HKDF_INFO_HEADER)?;
Ok(DerivedSubkeys {
payload_key: PayloadKey::from_zeroizing(payload_bytes),
header_key: HeaderKey::from_zeroizing(header_bytes),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::stream::STREAM_NONCE_SIZE;
#[test]
fn file_key_generate_has_correct_size() {
let key = FileKey::generate().unwrap();
assert_eq!(key.expose().len(), FILE_KEY_SIZE);
}
#[test]
fn file_key_generate_is_random() {
let a = FileKey::generate().unwrap();
let b = FileKey::generate().unwrap();
assert_ne!(
a.expose(),
b.expose(),
"two consecutive file keys must differ"
);
}
#[test]
fn derive_subkeys_round_trip() {
let file_key = FileKey::from_bytes_for_tests([0x11u8; FILE_KEY_SIZE]);
let nonce = [0x22u8; STREAM_NONCE_SIZE];
let a = derive_subkeys(&file_key, &nonce).unwrap();
let b = derive_subkeys(&file_key, &nonce).unwrap();
assert_eq!(a.payload_key.expose(), b.payload_key.expose());
assert_eq!(a.header_key.expose(), b.header_key.expose());
}
#[test]
fn derive_subkeys_payload_depends_on_stream_nonce() {
let file_key = FileKey::from_bytes_for_tests([0x11u8; FILE_KEY_SIZE]);
let nonce_a = [0x22u8; STREAM_NONCE_SIZE];
let nonce_b = [0x33u8; STREAM_NONCE_SIZE];
let a = derive_subkeys(&file_key, &nonce_a).unwrap();
let b = derive_subkeys(&file_key, &nonce_b).unwrap();
assert_ne!(
a.payload_key.expose(),
b.payload_key.expose(),
"payload key depends on stream_nonce"
);
assert_eq!(
a.header_key.expose(),
b.header_key.expose(),
"header key is independent of stream_nonce"
);
}
#[test]
fn derive_subkeys_depends_on_file_key() {
let file_a = FileKey::from_bytes_for_tests([0x11u8; FILE_KEY_SIZE]);
let file_b = FileKey::from_bytes_for_tests([0x33u8; FILE_KEY_SIZE]);
let nonce = [0x22u8; STREAM_NONCE_SIZE];
let a = derive_subkeys(&file_a, &nonce).unwrap();
let b = derive_subkeys(&file_b, &nonce).unwrap();
assert_ne!(a.payload_key.expose(), b.payload_key.expose());
}
#[test]
fn random_bytes_produces_different_outputs() {
let a = random_bytes::<32>().unwrap();
let b = random_bytes::<32>().unwrap();
assert_ne!(a, b);
}
#[test]
fn random_secret_has_correct_size_and_is_random() {
let a = random_secret::<24>().unwrap();
let b = random_secret::<24>().unwrap();
assert_eq!(a.len(), 24);
assert_ne!(*a, *b);
}
#[test]
fn hkdf_info_strings_are_canonical() {
assert_eq!(HKDF_INFO_PAYLOAD, b"ferrocrypt/v1/payload");
assert_eq!(HKDF_INFO_HEADER, b"ferrocrypt/v1/header");
}
}