use crate::{errors::HeaderError, v2::key::KeyDerivationParams};
#[derive(Debug, Clone)]
pub struct FileHeader {
pub magic: [u8; 6], pub version: u8, pub header_length: u32, pub salt: [u8; 16], pub kdf_memory: u32, pub kdf_iterations: u32, pub kdf_parallelism: u32, pub kdf_key_length: u8, pub content_nonce: [u8; 24], pub filename_nonce: [u8; 24], pub filename_ciphertext_length: u16, pub filename_ciphertext: Vec<u8>, }
pub const MAGIC: [u8; 6] = *b"SHADOW";
pub const VERSION: u8 = 2;
impl FileHeader {
pub fn new(
salt: [u8; 16],
kdf_params: KeyDerivationParams,
content_nonce: [u8; 24],
filename_nonce: [u8; 24],
filename_ciphertext: Vec<u8>,
) -> Result<Self, HeaderError> {
let filename_ciphertext_length: u16 = filename_ciphertext
.len()
.try_into()
.map_err(|_| HeaderError::FilenameTooLong)?;
let size = Self::min_length() + filename_ciphertext.len();
Ok(FileHeader {
magic: MAGIC,
version: VERSION,
header_length: size as u32,
salt,
kdf_memory: kdf_params.memory_cost,
kdf_iterations: kdf_params.time_cost,
kdf_parallelism: kdf_params.parallelism,
kdf_key_length: kdf_params.key_size,
content_nonce,
filename_nonce,
filename_ciphertext_length,
filename_ciphertext,
})
}
pub fn min_length() -> usize {
6 + 1 + 4 + 16 + 4 + 4 + 4 + 1 + 24 + 24 + 2 }
pub fn binding(&self) -> HeaderBinding<'_> {
HeaderBinding {
salt: &self.salt,
kdf_memory: self.kdf_memory,
kdf_iterations: self.kdf_iterations,
kdf_parallelism: self.kdf_parallelism,
kdf_key_length: self.kdf_key_length,
content_nonce: &self.content_nonce,
filename_nonce: &self.filename_nonce,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AadPurpose {
Filename,
Content,
}
impl AadPurpose {
fn domain_tag(self) -> &'static [u8] {
match self {
AadPurpose::Filename => b"shadow-crypt/v2/filename",
AadPurpose::Content => b"shadow-crypt/v2/content",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HeaderBinding<'a> {
pub salt: &'a [u8; 16],
pub kdf_memory: u32,
pub kdf_iterations: u32,
pub kdf_parallelism: u32,
pub kdf_key_length: u8,
pub content_nonce: &'a [u8; 24],
pub filename_nonce: &'a [u8; 24],
}
impl<'a> HeaderBinding<'a> {
pub fn new(
salt: &'a [u8; 16],
kdf_params: &KeyDerivationParams,
content_nonce: &'a [u8; 24],
filename_nonce: &'a [u8; 24],
) -> Self {
Self {
salt,
kdf_memory: kdf_params.memory_cost,
kdf_iterations: kdf_params.time_cost,
kdf_parallelism: kdf_params.parallelism,
kdf_key_length: kdf_params.key_size,
content_nonce,
filename_nonce,
}
}
pub fn aad(&self, purpose: AadPurpose) -> Vec<u8> {
let mut aad = Vec::with_capacity(FileHeader::min_length() + 24);
aad.extend_from_slice(&MAGIC);
aad.push(VERSION);
aad.extend_from_slice(self.salt);
aad.extend_from_slice(&self.kdf_memory.to_le_bytes());
aad.extend_from_slice(&self.kdf_iterations.to_le_bytes());
aad.extend_from_slice(&self.kdf_parallelism.to_le_bytes());
aad.push(self.kdf_key_length);
aad.extend_from_slice(self.content_nonce);
aad.extend_from_slice(self.filename_nonce);
aad.extend_from_slice(purpose.domain_tag());
aad
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile;
fn get_test_params() -> KeyDerivationParams {
KeyDerivationParams::from(profile::SecurityProfile::Test)
}
#[test]
fn default_values_are_correct() {
let header = FileHeader::new(
[0u8; 16],
get_test_params(),
[0u8; 24],
[0u8; 24],
vec![1, 2, 3, 4],
)
.unwrap();
assert_eq!(&header.magic, b"SHADOW");
assert_eq!(header.version, 2);
}
#[test]
fn header_size_is_calculated_correctly() {
let filename_ciphertext = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let header = FileHeader::new(
[0u8; 16],
get_test_params(),
[0u8; 24],
[0u8; 24],
filename_ciphertext.clone(),
)
.unwrap();
let expected_size: u32 = 90 + filename_ciphertext.len() as u32;
assert_eq!(header.header_length, expected_size);
}
#[test]
fn oversized_filename_ciphertext_is_rejected() {
let filename_ciphertext = vec![0u8; u16::MAX as usize + 1];
let result = FileHeader::new(
[0u8; 16],
get_test_params(),
[0u8; 24],
[0u8; 24],
filename_ciphertext,
);
assert!(matches!(result, Err(HeaderError::FilenameTooLong)));
}
#[test]
fn max_length_filename_ciphertext_is_accepted() {
let filename_ciphertext = vec![0u8; u16::MAX as usize];
let header = FileHeader::new(
[0u8; 16],
get_test_params(),
[0u8; 24],
[0u8; 24],
filename_ciphertext,
)
.unwrap();
assert_eq!(header.filename_ciphertext_length, u16::MAX);
}
#[test]
fn aad_differs_by_purpose() {
let salt = [1u8; 16];
let params = get_test_params();
let content_nonce = [2u8; 24];
let filename_nonce = [3u8; 24];
let binding = HeaderBinding::new(&salt, ¶ms, &content_nonce, &filename_nonce);
assert_ne!(
binding.aad(AadPurpose::Filename),
binding.aad(AadPurpose::Content)
);
}
#[test]
fn header_binding_matches_standalone_binding() {
let salt = [1u8; 16];
let params = get_test_params();
let content_nonce = [2u8; 24];
let filename_nonce = [3u8; 24];
let standalone = HeaderBinding::new(&salt, ¶ms, &content_nonce, &filename_nonce);
let header =
FileHeader::new(salt, params, content_nonce, filename_nonce, vec![1, 2, 3]).unwrap();
assert_eq!(
standalone.aad(AadPurpose::Content),
header.binding().aad(AadPurpose::Content)
);
assert_eq!(
standalone.aad(AadPurpose::Filename),
header.binding().aad(AadPurpose::Filename)
);
}
}