mod header;
#[cfg(test)]
mod tests;
pub use header::SealHeader;
use aes_gcm::Aes256Gcm;
use aes_gcm::aead::{Aead, AeadInOut, KeyInit, Payload};
use chacha20poly1305::ChaCha20Poly1305;
use hkdf::Hkdf;
use sha2::Sha256;
use zeroize::Zeroizing;
use crate::{Error, Result};
pub const HEADER_BYTES: usize = 42;
pub const TAG_BYTES: usize = 16;
pub const ROOT_KEY_BYTES: usize = 32;
const MAX_UNIT_BYTES: u64 = (1 << 36) - 32;
const LABEL: &[u8] = b"tessarikv/sealed-file/v1/";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Cipher {
Aes256Gcm,
ChaCha20Poly1305,
}
impl Cipher {
#[must_use]
pub fn preferred() -> Self {
if aes_instructions() {
Self::Aes256Gcm
} else {
Self::ChaCha20Poly1305
}
}
const fn code(self) -> u8 {
match self {
Self::Aes256Gcm => 1,
Self::ChaCha20Poly1305 => 2,
}
}
const fn from_code(code: u8) -> Option<Self> {
match code {
1 => Some(Self::Aes256Gcm),
2 => Some(Self::ChaCha20Poly1305),
_ => None,
}
}
}
#[cfg(target_arch = "x86_64")]
fn aes_instructions() -> bool {
std::arch::is_x86_feature_detected!("aes")
}
#[cfg(target_arch = "aarch64")]
fn aes_instructions() -> bool {
std::arch::is_aarch64_feature_detected!("aes")
}
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
fn aes_instructions() -> bool {
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FileKind {
Table,
Log,
Manifest,
Options,
KeyCheck,
}
impl FileKind {
const fn label(self) -> &'static [u8] {
match self {
Self::Table => b"table",
Self::Log => b"log",
Self::Manifest => b"manifest",
Self::Options => b"options",
Self::KeyCheck => b"key-check",
}
}
}
pub struct RootKey(Zeroizing<[u8; ROOT_KEY_BYTES]>);
impl RootKey {
#[must_use]
pub fn new(bytes: [u8; ROOT_KEY_BYTES]) -> Self {
Self(Zeroizing::new(bytes))
}
}
impl std::fmt::Debug for RootKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("RootKey(..)")
}
}
pub struct FileSealer {
aead: SealingAead,
context: Vec<u8>,
}
enum SealingAead {
Aes(Box<Aes256Gcm>),
ChaCha(Box<ChaCha20Poly1305>),
}
impl FileSealer {
#[must_use]
pub fn new(root: &RootKey, header: &SealHeader, kind: FileKind) -> Self {
let encoded = header.encode();
let mut label = Vec::with_capacity(LABEL.len().saturating_add(kind.label().len()));
label.extend_from_slice(LABEL);
label.extend_from_slice(kind.label());
let mut key = Zeroizing::new([0_u8; 32]);
let _ = Hkdf::<Sha256>::new(Some(&encoded), root.0.as_slice())
.expand(&label, key.as_mut_slice());
let aead = match header.cipher {
Cipher::Aes256Gcm => SealingAead::Aes(Box::new(Aes256Gcm::new((&*key).into()))),
Cipher::ChaCha20Poly1305 => {
SealingAead::ChaCha(Box::new(ChaCha20Poly1305::new((&*key).into())))
}
};
let mut context = Vec::with_capacity(HEADER_BYTES.saturating_add(label.len()));
context.extend_from_slice(&encoded);
context.extend_from_slice(&label);
Self { aead, context }
}
pub fn seal(&self, position: u64, plaintext: &[u8]) -> Result<Vec<u8>> {
let aad = self.aad(position);
let nonce = nonce(position);
let payload = Payload {
msg: plaintext,
aad: &aad,
};
match &self.aead {
SealingAead::Aes(aead) => aead.encrypt((&nonce).into(), payload),
SealingAead::ChaCha(aead) => aead.encrypt((&nonce).into(), payload),
}
.map_err(|_| too_large(plaintext.len()))
}
pub fn seal_in_place(&self, position: u64, buffer: &mut [u8]) -> Result<[u8; TAG_BYTES]> {
let aad = self.aad(position);
let nonce = nonce(position);
let len = buffer.len();
let tag = match &self.aead {
SealingAead::Aes(aead) => {
aead.encrypt_inout_detached((&nonce).into(), &aad, buffer.into())
}
SealingAead::ChaCha(aead) => {
aead.encrypt_inout_detached((&nonce).into(), &aad, buffer.into())
}
}
.map_err(|_| too_large(len))?;
let mut bytes = [0; TAG_BYTES];
bytes.copy_from_slice(&tag);
Ok(bytes)
}
pub fn open(&self, position: u64, sealed: &[u8]) -> Result<Vec<u8>> {
let aad = self.aad(position);
let nonce = nonce(position);
let payload = Payload {
msg: sealed,
aad: &aad,
};
match &self.aead {
SealingAead::Aes(aead) => aead.decrypt((&nonce).into(), payload),
SealingAead::ChaCha(aead) => aead.decrypt((&nonce).into(), payload),
}
.map_err(|_| Error::Unauthenticated {
what: "sealed unit",
})
}
fn aad(&self, position: u64) -> Vec<u8> {
let mut aad = Vec::with_capacity(self.context.len().saturating_add(8));
aad.extend_from_slice(&self.context);
aad.extend_from_slice(&position.to_le_bytes());
aad
}
}
impl std::fmt::Debug for FileSealer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("FileSealer(..)")
}
}
fn too_large(len: usize) -> Error {
Error::TooLarge {
what: "sealed unit",
size: u64::try_from(len).unwrap_or(u64::MAX),
limit: MAX_UNIT_BYTES,
}
}
fn nonce(position: u64) -> [u8; 12] {
let mut nonce = [0; 12];
if let Some(start) = nonce.get_mut(..8) {
start.copy_from_slice(&position.to_le_bytes());
}
nonce
}