use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct CipherError {
message: String,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
}
impl CipherError {
pub fn msg(message: impl fmt::Display) -> Self {
Self {
message: message.to_string(),
source: None,
}
}
pub fn with_source<E>(message: impl fmt::Display, source: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self {
message: message.to_string(),
source: Some(Arc::new(source)),
}
}
}
impl fmt::Display for CipherError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for CipherError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|s| -> &(dyn std::error::Error + 'static) { s })
}
}
pub trait SegmentCipher: Send + Sync {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CipherError>;
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CipherError>;
}
#[cfg(feature = "encryption")]
mod private {
use super::{CipherError, SegmentCipher};
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone)]
struct AeadError(String);
impl fmt::Display for AeadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for AeadError {}
fn wrap<E: fmt::Display>(message: &'static str, e: E) -> CipherError {
CipherError {
message: message.to_string(),
source: Some(Arc::new(AeadError(e.to_string()))),
}
}
pub struct AesGcmCipher {
cipher: aes_gcm::Aes256Gcm,
}
impl AesGcmCipher {
pub fn from_slice(key_bytes: &[u8]) -> Result<Self, CipherError> {
use aes_gcm::KeyInit;
let cipher = aes_gcm::Aes256Gcm::new_from_slice(key_bytes)
.map_err(|e| wrap("invalid AES-256 key", e))?;
Ok(Self { cipher })
}
pub fn new(key_bytes: &[u8; 32]) -> Self {
use aes_gcm::KeyInit;
Self {
cipher: aes_gcm::Aes256Gcm::new_from_slice(key_bytes)
.expect("32-byte key is always valid for AES-256"),
}
}
}
impl SegmentCipher for AesGcmCipher {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CipherError> {
use aes_gcm::aead::Aead;
use rand::Rng;
let mut nonce_bytes = [0u8; 12];
rand::rng().fill_bytes(&mut nonce_bytes);
let nonce = aes_gcm::Nonce::from(nonce_bytes);
let ciphertext = self
.cipher
.encrypt(&nonce, plaintext)
.map_err(|e| wrap("AES-GCM encryption failed", e))?;
let mut out = Vec::with_capacity(12 + ciphertext.len());
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CipherError> {
use aes_gcm::aead::Aead;
if ciphertext.len() < 12 {
return Err(CipherError::msg("ciphertext too small for nonce prefix"));
}
let (nonce_bytes, encrypted) = ciphertext.split_at(12);
let nonce: [u8; 12] = nonce_bytes
.try_into()
.map_err(|_| CipherError::msg("invalid nonce length: expected 12 bytes"))?;
let nonce = aes_gcm::Nonce::from(nonce);
self.cipher
.decrypt(&nonce, encrypted)
.map_err(|e| wrap("AES-GCM decryption failed", e))
}
}
const XCHACHA_NONCE_LEN: usize = 24;
pub struct XChaCha20Poly1305Cipher {
cipher: chacha20poly1305::XChaCha20Poly1305,
}
impl XChaCha20Poly1305Cipher {
pub fn new(key_bytes: &[u8; 32]) -> Self {
use chacha20poly1305::KeyInit;
Self {
cipher: chacha20poly1305::XChaCha20Poly1305::new_from_slice(key_bytes)
.expect("32-byte key is always valid for XChaCha20-Poly1305"),
}
}
pub fn from_slice(key_bytes: &[u8]) -> Result<Self, CipherError> {
use chacha20poly1305::KeyInit;
let cipher = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key_bytes)
.map_err(|e| wrap("invalid XChaCha20 key", e))?;
Ok(Self { cipher })
}
}
impl SegmentCipher for XChaCha20Poly1305Cipher {
fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CipherError> {
use chacha20poly1305::aead::Aead;
use rand::Rng;
let mut nonce_bytes = [0u8; XCHACHA_NONCE_LEN];
rand::rng().fill_bytes(&mut nonce_bytes);
let nonce = chacha20poly1305::XNonce::from(nonce_bytes);
let ciphertext = self
.cipher
.encrypt(&nonce, plaintext)
.map_err(|e| wrap("XChaCha20 encryption failed", e))?;
let mut out = Vec::with_capacity(XCHACHA_NONCE_LEN + ciphertext.len());
out.extend_from_slice(&nonce_bytes);
out.extend_from_slice(&ciphertext);
Ok(out)
}
fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CipherError> {
use chacha20poly1305::aead::Aead;
if ciphertext.len() < XCHACHA_NONCE_LEN {
return Err(CipherError::msg(
"ciphertext too small for XChaCha20 nonce prefix (need 24 bytes)",
));
}
let (nonce_bytes, encrypted) = ciphertext.split_at(XCHACHA_NONCE_LEN);
let nonce: [u8; XCHACHA_NONCE_LEN] = nonce_bytes
.try_into()
.map_err(|_| CipherError::msg("invalid nonce length: expected 24 bytes"))?;
let nonce = chacha20poly1305::XNonce::from(nonce);
self.cipher
.decrypt(&nonce, encrypted)
.map_err(|e| wrap("XChaCha20 decryption failed", e))
}
}
}
#[cfg(feature = "encryption")]
pub use private::{AesGcmCipher, XChaCha20Poly1305Cipher};