use std::convert::TryInto;
use crate::cipher::TAG_SIZE;
use crate::error::{KeystoreError, Result};
pub const FORMAT_VERSION_V1: u16 = 0x0001;
pub(crate) const HEADER_SIZE: usize = 6 + 2 + 2 + 1 + 4 + 4 + 1 + 1 + 16 + 12 + 4;
pub(crate) const FOOTER_SIZE: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CipherId {
Aes256Gcm = 0x01,
}
impl CipherId {
fn from_byte(b: u8) -> Result<Self> {
match b {
0x01 => Ok(Self::Aes256Gcm),
other => Err(KeystoreError::UnsupportedCipher(other)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum KdfId {
Argon2id = 0x01,
}
impl KdfId {
fn from_byte(b: u8) -> Result<Self> {
match b {
0x01 => Ok(Self::Argon2id),
other => Err(KeystoreError::UnsupportedKdf(other)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KdfParams {
pub id: KdfId,
pub memory_kib: u32,
pub iterations: u32,
pub lanes: u8,
}
impl KdfParams {
pub const DEFAULT: Self = Self {
id: KdfId::Argon2id,
memory_kib: 65536,
iterations: 3,
lanes: 4,
};
pub const STRONG: Self = Self {
id: KdfId::Argon2id,
memory_kib: 262144,
iterations: 4,
lanes: 4,
};
#[doc(hidden)]
pub const FAST_TEST: Self = Self {
id: KdfId::Argon2id,
memory_kib: 8 * 1024,
iterations: 1,
lanes: 1,
};
}
impl Default for KdfParams {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeystoreHeader {
pub magic: [u8; 6],
pub format_version: u16,
pub scheme_id: u16,
pub kdf: KdfParams,
pub cipher: CipherId,
pub salt: [u8; 16],
pub nonce: [u8; 12],
pub payload_len: u32,
}
impl KeystoreHeader {
pub(crate) fn encode(&self) -> [u8; HEADER_SIZE] {
let mut out = [0u8; HEADER_SIZE];
let mut i = 0;
out[i..i + 6].copy_from_slice(&self.magic);
i += 6;
out[i..i + 2].copy_from_slice(&self.format_version.to_be_bytes());
i += 2;
out[i..i + 2].copy_from_slice(&self.scheme_id.to_be_bytes());
i += 2;
out[i] = self.kdf.id as u8;
i += 1;
out[i..i + 4].copy_from_slice(&self.kdf.memory_kib.to_be_bytes());
i += 4;
out[i..i + 4].copy_from_slice(&self.kdf.iterations.to_be_bytes());
i += 4;
out[i] = self.kdf.lanes;
i += 1;
out[i] = self.cipher as u8;
i += 1;
out[i..i + 16].copy_from_slice(&self.salt);
i += 16;
out[i..i + 12].copy_from_slice(&self.nonce);
i += 12;
out[i..i + 4].copy_from_slice(&self.payload_len.to_be_bytes());
i += 4;
debug_assert_eq!(i, HEADER_SIZE);
out
}
pub(crate) fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.len() < HEADER_SIZE {
return Err(KeystoreError::Truncated {
claimed: HEADER_SIZE,
available: bytes.len(),
});
}
let mut i = 0;
let magic: [u8; 6] = bytes[i..i + 6].try_into().unwrap();
i += 6;
if !is_known_magic(&magic) {
return Err(KeystoreError::UnknownMagic { saw: magic });
}
let format_version = u16::from_be_bytes(bytes[i..i + 2].try_into().unwrap());
i += 2;
if format_version != FORMAT_VERSION_V1 {
return Err(KeystoreError::UnsupportedFormat {
found: format_version,
});
}
let scheme_id = u16::from_be_bytes(bytes[i..i + 2].try_into().unwrap());
i += 2;
let kdf_id = KdfId::from_byte(bytes[i])?;
i += 1;
let memory_kib = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap());
i += 4;
let iterations = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap());
i += 4;
let lanes = bytes[i];
i += 1;
let cipher = CipherId::from_byte(bytes[i])?;
i += 1;
let salt: [u8; 16] = bytes[i..i + 16].try_into().unwrap();
i += 16;
let nonce: [u8; 12] = bytes[i..i + 12].try_into().unwrap();
i += 12;
let payload_len = u32::from_be_bytes(bytes[i..i + 4].try_into().unwrap());
i += 4;
debug_assert_eq!(i, HEADER_SIZE);
Ok(Self {
magic,
format_version,
scheme_id,
kdf: KdfParams {
id: kdf_id,
memory_kib,
iterations,
lanes,
},
cipher,
salt,
nonce,
payload_len,
})
}
}
fn is_known_magic(m: &[u8; 6]) -> bool {
matches!(m, b"DIGVK1" | b"DIGLW1")
}
pub(crate) fn encode_file(header: &KeystoreHeader, ciphertext_and_tag: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_SIZE + ciphertext_and_tag.len() + FOOTER_SIZE);
out.extend_from_slice(&header.encode());
out.extend_from_slice(ciphertext_and_tag);
let crc = crc32fast::hash(&out);
out.extend_from_slice(&crc.to_be_bytes());
out
}
pub(crate) fn decode_file(bytes: &[u8]) -> Result<(KeystoreHeader, Vec<u8>, [u8; HEADER_SIZE])> {
if bytes.len() < HEADER_SIZE + TAG_SIZE + FOOTER_SIZE {
return Err(KeystoreError::Truncated {
claimed: HEADER_SIZE + TAG_SIZE + FOOTER_SIZE,
available: bytes.len(),
});
}
let crc_stored = u32::from_be_bytes(bytes[bytes.len() - 4..].try_into().unwrap());
let crc_computed = crc32fast::hash(&bytes[..bytes.len() - 4]);
if crc_stored != crc_computed {
return Err(KeystoreError::CrcMismatch {
stored: crc_stored,
computed: crc_computed,
});
}
let header_bytes: [u8; HEADER_SIZE] = bytes[..HEADER_SIZE].try_into().unwrap();
let header = KeystoreHeader::decode(&header_bytes)?;
let payload_start = HEADER_SIZE;
let payload_end = payload_start + header.payload_len as usize;
if payload_end + FOOTER_SIZE > bytes.len() {
return Err(KeystoreError::Truncated {
claimed: payload_end + FOOTER_SIZE,
available: bytes.len(),
});
}
let ciphertext_and_tag = bytes[payload_start..payload_end].to_vec();
Ok((header, ciphertext_and_tag, header_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_header() -> KeystoreHeader {
KeystoreHeader {
magic: *b"DIGVK1",
format_version: FORMAT_VERSION_V1,
scheme_id: 0x0001,
kdf: KdfParams::FAST_TEST,
cipher: CipherId::Aes256Gcm,
salt: [9u8; 16],
nonce: [2u8; 12],
payload_len: 48,
}
}
#[test]
fn header_roundtrip() {
let h = sample_header();
let bytes = h.encode();
assert_eq!(bytes.len(), HEADER_SIZE);
let h2 = KeystoreHeader::decode(&bytes).unwrap();
assert_eq!(h, h2);
}
#[test]
fn unknown_magic_rejected() {
let mut bytes = sample_header().encode();
bytes[0] = b'X';
let err = KeystoreHeader::decode(&bytes).unwrap_err();
assert!(matches!(err, KeystoreError::UnknownMagic { .. }));
}
#[test]
fn bad_format_version_rejected() {
let mut h = sample_header();
h.format_version = 999;
let err = KeystoreHeader::decode(&h.encode()).unwrap_err();
assert!(matches!(
err,
KeystoreError::UnsupportedFormat { found: 999 }
));
}
#[test]
fn bad_kdf_id_rejected() {
let mut bytes = sample_header().encode();
bytes[10] = 0xFF;
let err = KeystoreHeader::decode(&bytes).unwrap_err();
assert!(matches!(err, KeystoreError::UnsupportedKdf(0xFF)));
}
#[test]
fn bad_cipher_id_rejected() {
let mut bytes = sample_header().encode();
bytes[20] = 0xFE;
let err = KeystoreHeader::decode(&bytes).unwrap_err();
assert!(matches!(err, KeystoreError::UnsupportedCipher(0xFE)));
}
#[test]
fn file_roundtrip_valid_crc() {
let h = sample_header();
let payload = vec![0x42u8; h.payload_len as usize];
let bytes = encode_file(&h, &payload);
let (h2, pl2, _) = decode_file(&bytes).unwrap();
assert_eq!(h2, h);
assert_eq!(pl2, payload);
}
#[test]
fn crc_mismatch_detected() {
let h = sample_header();
let payload = vec![0x42u8; h.payload_len as usize];
let mut bytes = encode_file(&h, &payload);
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
let err = decode_file(&bytes).unwrap_err();
assert!(matches!(err, KeystoreError::CrcMismatch { .. }));
}
#[test]
fn truncated_file_rejected() {
let err = decode_file(&[0u8; 10]).unwrap_err();
assert!(matches!(err, KeystoreError::Truncated { .. }));
}
#[test]
fn header_size_constant_correct() {
assert_eq!(sample_header().encode().len(), HEADER_SIZE);
assert_eq!(HEADER_SIZE, 53);
}
}