//! sym.rs — Stateless symmetric encryption (Phase 1) //! //! Commands implemented (RESP): //! - SYM KEYGEN //! - SYM ENCRYPT //! - SYM DECRYPT //! //! Notes: //! - Raw key: exactly 32 bytes, provided as Base64 in commands. //! - Cipher: XChaCha20-Poly1305 (AEAD) without AAD in Phase 1 //! - Ciphertext binary layout: [version:1][nonce:24][ciphertext||tag] //! - Encoding for wire I/O: Base64 use base64::{engine::general_purpose::STANDARD as B64, Engine as _}; use chacha20poly1305::{ aead::{Aead, KeyInit, OsRng}, XChaCha20Poly1305, XNonce, }; use rand::RngCore; use crate::protocol::Protocol; const VERSION: u8 = 1; const NONCE_LEN: usize = 24; const TAG_LEN: usize = 16; #[derive(Debug)] pub enum SymWireError { InvalidKey, BadEncoding, BadFormat, BadVersion(u8), Crypto, } impl SymWireError { fn to_protocol(self) -> Protocol { match self { SymWireError::InvalidKey => Protocol::err("ERR sym: invalid key"), SymWireError::BadEncoding => Protocol::err("ERR sym: bad encoding"), SymWireError::BadFormat => Protocol::err("ERR sym: bad format"), SymWireError::BadVersion(v) => Protocol::err(&format!("ERR sym: unsupported version {}", v)), SymWireError::Crypto => Protocol::err("ERR sym: auth failed"), } } } fn decode_key_b64(s: &str) -> Result { let bytes = B64.decode(s.as_bytes()).map_err(|_| SymWireError::BadEncoding)?; if bytes.len() != 32 { return Err(SymWireError::InvalidKey); } Ok(chacha20poly1305::Key::from_slice(&bytes).to_owned()) } fn encrypt_blob(key: &chacha20poly1305::Key, plaintext: &[u8]) -> Result, SymWireError> { let cipher = XChaCha20Poly1305::new(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).map_err(|_| SymWireError::Crypto)?; out.extend_from_slice(&ct); Ok(out) } fn decrypt_blob(key: &chacha20poly1305::Key, blob: &[u8]) -> Result, SymWireError> { if blob.len() < 1 + NONCE_LEN + TAG_LEN { return Err(SymWireError::BadFormat); } let ver = blob[0]; if ver != VERSION { return Err(SymWireError::BadVersion(ver)); } let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]); let ct = &blob[1 + NONCE_LEN..]; let cipher = XChaCha20Poly1305::new(key); cipher.decrypt(nonce, ct).map_err(|_| SymWireError::Crypto) } // ---------- Command handlers (RESP) ---------- pub async fn cmd_sym_keygen() -> Protocol { let mut key_bytes = [0u8; 32]; OsRng.fill_bytes(&mut key_bytes); let key_b64 = B64.encode(key_bytes); Protocol::BulkString(key_b64) } pub async fn cmd_sym_encrypt(key_b64: &str, message: &str) -> Protocol { let key = match decode_key_b64(key_b64) { Ok(k) => k, Err(e) => return e.to_protocol(), }; match encrypt_blob(&key, message.as_bytes()) { Ok(blob) => Protocol::BulkString(B64.encode(blob)), Err(e) => e.to_protocol(), } } pub async fn cmd_sym_decrypt(key_b64: &str, ct_b64: &str) -> Protocol { let key = match decode_key_b64(key_b64) { Ok(k) => k, Err(e) => return e.to_protocol(), }; let blob = match B64.decode(ct_b64.as_bytes()) { Ok(b) => b, Err(_) => return SymWireError::BadEncoding.to_protocol(), }; match decrypt_blob(&key, &blob) { Ok(pt) => match String::from_utf8(pt) { Ok(s) => Protocol::BulkString(s), Err(_) => Protocol::err("ERR sym: invalid UTF-8 plaintext"), }, Err(e) => e.to_protocol(), } }