pub mod aad;
#[cfg(feature = "encryption")]
pub mod aead;
#[cfg(all(feature = "encryption", zstd_any))]
pub mod block;
pub mod error;
pub mod key_chain;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[cfg(all(feature = "encryption", zstd_any))]
pub use block::{
DecryptedBlock, EncryptedBlockMetadata, decrypt_block, encrypt_block,
parse_encrypted_block_metadata, reconstruct_block_aad,
};
pub use error::DecryptError;
pub use key_chain::KeyChain;
#[cfg(feature = "std")]
pub use key_chain::StaticKeyChain;
pub trait EncryptionProvider:
Send + Sync + core::panic::UnwindSafe + core::panic::RefUnwindSafe
{
fn encrypt(&self, plaintext: &[u8]) -> crate::Result<Vec<u8>>;
fn max_overhead(&self) -> u32;
#[must_use]
fn supports_aad_block_path(&self) -> bool {
false
}
fn decrypt(&self, ciphertext: &[u8]) -> crate::Result<Vec<u8>>;
fn encrypt_vec(&self, plaintext: Vec<u8>) -> crate::Result<Vec<u8>> {
self.encrypt(&plaintext)
}
fn decrypt_vec(&self, ciphertext: Vec<u8>) -> crate::Result<Vec<u8>> {
self.decrypt(&ciphertext)
}
fn encrypt_block_aad(
&self,
_plaintext: &[u8],
_identity: &crate::table::block::BlockIdentity,
_compression_type: u8,
_block_flags: u8,
) -> crate::Result<Vec<u8>> {
Err(crate::Error::Encrypt(
"provider does not support AAD-bound block encryption",
))
}
fn decrypt_block_aad(
&self,
_ciphertext: &[u8],
_identity: &crate::table::block::BlockIdentity,
) -> crate::Result<Vec<u8>> {
Err(crate::Error::Decrypt(
"provider does not support AAD-bound block decryption",
))
}
}
#[cfg(feature = "encryption")]
pub struct Aes256GcmProvider {
cipher: aes_gcm::Aes256Gcm,
key_chain: crate::encryption::key_chain::StaticKeyChain,
key_epoch: u8,
suite_id: crate::encryption::aad::SuiteId,
}
#[cfg(feature = "encryption")]
impl Aes256GcmProvider {
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
pub const OVERHEAD: usize = Self::NONCE_LEN + Self::TAG_LEN;
#[must_use]
pub fn new(key: &[u8; 32]) -> Self {
use aes_gcm::KeyInit;
Self {
cipher: aes_gcm::Aes256Gcm::new(key.into()),
key_chain: crate::encryption::key_chain::StaticKeyChain::new().with_key(0, *key),
key_epoch: 0,
suite_id: crate::encryption::aad::SuiteId::Aes256Gcm,
}
}
pub fn from_slice(key: &[u8]) -> crate::Result<Self> {
let key: &[u8; 32] = key
.try_into()
.map_err(|_| crate::Error::Encrypt("AES-256-GCM key must be exactly 32 bytes"))?;
Ok(Self::new(key))
}
}
#[cfg(feature = "encryption")]
fn new_chacha_rng() -> rand_chacha::ChaCha20Rng {
use aes_gcm::aead::Generate;
use aes_gcm::aead::rand_core::SeedableRng;
let seed: [u8; 32] = <[u8; 32]>::generate();
rand_chacha::ChaCha20Rng::from_seed(seed)
}
#[cfg(feature = "encryption")]
fn process_pid() -> u32 {
#[cfg(feature = "std")]
{
std::process::id()
}
#[cfg(not(feature = "std"))]
{
0
}
}
#[cfg(feature = "encryption")]
struct ForkAwareRng {
pid: core::cell::Cell<u32>,
rng: core::cell::RefCell<rand_chacha::ChaCha20Rng>,
}
#[cfg(feature = "encryption")]
impl ForkAwareRng {
fn new() -> Self {
Self {
pid: core::cell::Cell::new(process_pid()),
rng: core::cell::RefCell::new(new_chacha_rng()),
}
}
fn with_rng<R>(&self, f: impl FnOnce(&mut rand_chacha::ChaCha20Rng) -> R) -> R {
let mut rng_ref = self.rng.borrow_mut();
let current_pid = process_pid();
if self.pid.get() != current_pid {
self.pid.set(current_pid);
*rng_ref = new_chacha_rng();
}
f(&mut rng_ref)
}
}
#[cfg(feature = "encryption")]
thread_local! {
static THREAD_RNG: ForkAwareRng = ForkAwareRng::new();
}
#[cfg(feature = "encryption")]
fn thread_local_rng<R>(f: impl FnOnce(&mut rand_chacha::ChaCha20Rng) -> R) -> R {
THREAD_RNG.with(|state| state.with_rng(f))
}
#[cfg(feature = "encryption")]
impl EncryptionProvider for Aes256GcmProvider {
fn max_overhead(&self) -> u32 {
#[cfg(zstd_any)]
{
8 + 39 + 8 + 16
}
#[cfg(not(zstd_any))]
#[expect(clippy::cast_possible_truncation, reason = "OVERHEAD is 28")]
{
Self::OVERHEAD as u32
}
}
#[cfg(zstd_any)]
fn supports_aad_block_path(&self) -> bool {
true
}
fn encrypt(&self, plaintext: &[u8]) -> crate::Result<Vec<u8>> {
use aes_gcm::aead::{AeadInOut, Generate, Nonce};
let nonce = thread_local_rng(Nonce::<aes_gcm::Aes256Gcm>::generate_from_rng);
let mut buf = Vec::with_capacity(Self::NONCE_LEN + plaintext.len() + Self::TAG_LEN);
buf.extend_from_slice(&nonce);
buf.extend_from_slice(plaintext);
#[expect(
clippy::indexing_slicing,
reason = "buf length = NONCE_LEN + plaintext.len()"
)]
let tag = self
.cipher
.encrypt_inout_detached(&nonce, b"", (&mut buf[Self::NONCE_LEN..]).into())
.map_err(|_| crate::Error::Encrypt("AES-256-GCM encryption failed"))?;
buf.extend_from_slice(&tag);
Ok(buf)
}
fn decrypt(&self, ciphertext: &[u8]) -> crate::Result<Vec<u8>> {
use aes_gcm::aead::{AeadInOut, Nonce, Tag};
let min_len = Self::NONCE_LEN + Self::TAG_LEN;
if ciphertext.len() < min_len {
return Err(crate::Error::Decrypt(
"ciphertext too short for AES-256-GCM (need nonce + tag)",
));
}
#[expect(clippy::indexing_slicing, reason = "length checked above")]
let nonce = Nonce::<aes_gcm::Aes256Gcm>::try_from(&ciphertext[..Self::NONCE_LEN])
.map_err(|_| crate::Error::Decrypt("AES-256-GCM nonce length mismatch"))?;
let tag_start = ciphertext.len() - Self::TAG_LEN;
#[expect(clippy::indexing_slicing, reason = "length checked above")]
let tag = Tag::<aes_gcm::Aes256Gcm>::try_from(&ciphertext[tag_start..])
.map_err(|_| crate::Error::Decrypt("AES-256-GCM tag length mismatch"))?;
#[expect(clippy::indexing_slicing, reason = "length checked above")]
let mut buf = ciphertext[Self::NONCE_LEN..tag_start].to_vec();
self.cipher
.decrypt_inout_detached(&nonce, b"", (&mut buf[..]).into(), &tag)
.map_err(|_| {
crate::Error::Decrypt("AES-256-GCM decryption failed (bad key or tampered data)")
})?;
Ok(buf)
}
fn encrypt_vec(&self, mut buf: Vec<u8>) -> crate::Result<Vec<u8>> {
use aes_gcm::aead::{AeadInOut, Generate, Nonce};
let nonce = thread_local_rng(Nonce::<aes_gcm::Aes256Gcm>::generate_from_rng);
let plaintext_len = buf.len();
buf.reserve(Self::NONCE_LEN + Self::TAG_LEN);
buf.resize(plaintext_len + Self::NONCE_LEN, 0);
buf.copy_within(..plaintext_len, Self::NONCE_LEN);
#[expect(
clippy::indexing_slicing,
reason = "buf was just resized to include NONCE_LEN"
)]
buf[..Self::NONCE_LEN].copy_from_slice(&nonce);
#[expect(
clippy::indexing_slicing,
reason = "buf length ≥ NONCE_LEN after resize + copy_within"
)]
let tag = self
.cipher
.encrypt_inout_detached(&nonce, b"", (&mut buf[Self::NONCE_LEN..]).into())
.map_err(|_| crate::Error::Encrypt("AES-256-GCM encryption failed"))?;
buf.extend_from_slice(&tag);
Ok(buf)
}
fn decrypt_vec(&self, mut buf: Vec<u8>) -> crate::Result<Vec<u8>> {
use aes_gcm::aead::{AeadInOut, Nonce, Tag};
let min_len = Self::NONCE_LEN + Self::TAG_LEN;
if buf.len() < min_len {
return Err(crate::Error::Decrypt(
"ciphertext too short for AES-256-GCM (need nonce + tag)",
));
}
#[expect(clippy::indexing_slicing, reason = "length checked above")]
let nonce = Nonce::<aes_gcm::Aes256Gcm>::try_from(&buf[..Self::NONCE_LEN])
.map_err(|_| crate::Error::Decrypt("AES-256-GCM nonce length mismatch"))?;
let tag_start = buf.len() - Self::TAG_LEN;
#[expect(clippy::indexing_slicing, reason = "length checked above")]
let tag = Tag::<aes_gcm::Aes256Gcm>::try_from(&buf[tag_start..])
.map_err(|_| crate::Error::Decrypt("AES-256-GCM tag length mismatch"))?;
buf.copy_within(Self::NONCE_LEN..tag_start, 0);
buf.truncate(tag_start - Self::NONCE_LEN);
self.cipher
.decrypt_inout_detached(&nonce, b"", (&mut buf[..]).into(), &tag)
.map_err(|_| {
crate::Error::Decrypt("AES-256-GCM decryption failed (bad key or tampered data)")
})?;
Ok(buf)
}
#[cfg(zstd_any)]
fn encrypt_block_aad(
&self,
plaintext: &[u8],
identity: &crate::table::block::BlockIdentity,
compression_type: u8,
block_flags: u8,
) -> crate::Result<Vec<u8>> {
let ctx = crate::encryption::aad::EncryptionContext::v1(
self.key_epoch,
self.suite_id,
compression_type,
block_flags,
);
crate::encryption::encrypt_block(plaintext, identity, &ctx, &self.key_chain)
}
#[cfg(zstd_any)]
fn decrypt_block_aad(
&self,
ciphertext: &[u8],
identity: &crate::table::block::BlockIdentity,
) -> crate::Result<Vec<u8>> {
match crate::encryption::decrypt_block(ciphertext, identity, &self.key_chain) {
Ok(decrypted) => Ok(decrypted.plaintext),
Err(e) => {
log::debug!("AAD-bound block decryption failed: {e:?}");
Err(crate::Error::Decrypt(
"AAD-bound block decryption failed (tampered, wrong key, or malformed frame)",
))
}
}
}
}
#[cfg(test)]
#[allow(
clippy::doc_markdown,
clippy::redundant_clone,
clippy::unnecessary_wraps,
clippy::redundant_closure_for_method_calls
)]
mod tests;