use chacha20poly1305::{ aead::{Aead, KeyInit, OsRng}, XChaCha20Poly1305, XNonce, }; use rand::RngCore; use sha2::{Digest, Sha256}; const VERSION: u8 = 1; const NONCE_LEN: usize = 24; const TAG_LEN: usize = 16; #[derive(Debug)] pub enum CryptoError { Format, // wrong length / header Version(u8), // unknown version Decrypt, // wrong key or corrupted data } impl From for crate::error::DBError { fn from(e: CryptoError) -> Self { crate::error::DBError(format!("Crypto error: {:?}", e)) } } /// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes) #[derive(Clone)] pub struct CryptoFactory { key: chacha20poly1305::Key, } impl CryptoFactory { /// Accepts any secret bytes; turns them into a 32-byte key (SHA-256). pub fn new>(secret: S) -> Self { let mut h = Sha256::new(); h.update(b"xchacha20poly1305-factory:v1"); // domain separation h.update(secret.as_ref()); let digest = h.finalize(); // 32 bytes let key = chacha20poly1305::Key::from_slice(&digest).to_owned(); Self { key } } /// Output layout: [version:1][nonce:24][ciphertext||tag] pub fn encrypt(&self, plaintext: &[u8]) -> Vec { let cipher = XChaCha20Poly1305::new(&self.key); let mut nonce_bytes = [0u8; NONCE_LEN]; OsRng.fill_bytes(&mut nonce_bytes); let nonce = XNonce::from_slice(&nonce_bytes); let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN); out.push(VERSION); out.extend_from_slice(&nonce_bytes); let ct = cipher.encrypt(nonce, plaintext).expect("encrypt"); out.extend_from_slice(&ct); out } pub fn decrypt(&self, blob: &[u8]) -> Result, CryptoError> { if blob.len() < 1 + NONCE_LEN + TAG_LEN { return Err(CryptoError::Format); } let ver = blob[0]; if ver != VERSION { return Err(CryptoError::Version(ver)); } let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]); let ct = &blob[1 + NONCE_LEN..]; let cipher = XChaCha20Poly1305::new(&self.key); cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt) } }