From 7aff2e1d279a4e442b32f49ca0a0eca065355787 Mon Sep 17 00:00:00 2001 From: murilo ijanc Date: Wed, 25 Mar 2026 02:07:37 -0300 Subject: Initial commit: tesseras-paste decentralized pastebin DHT-backed encrypted pastebin with two binaries (tp/tpd), XChaCha20-Poly1305 encryption, content-addressed storage, and Unix socket + HTTP interfaces. --- src/crypto.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/crypto.rs (limited to 'src/crypto.rs') diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..1480040 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,84 @@ +//! XChaCha20-Poly1305 authenticated encryption. +//! +//! Uses OS-provided randomness (`arc4random_buf`) via +//! `tesseras_dht::sys::random_bytes` for key and nonce +//! generation. + +use chacha20poly1305::{ + XChaCha20Poly1305, XNonce, + aead::{Aead, KeyInit}, +}; + +/// XChaCha20 extended nonce size (24 bytes). +const NONCE_SIZE: usize = 24; + +/// XChaCha20-Poly1305 key size (32 bytes). +pub const KEY_SIZE: usize = 32; + +/// Generate a random 32-byte encryption key. +pub fn generate_key() -> [u8; KEY_SIZE] { + let mut key = [0u8; KEY_SIZE]; + tesseras_dht::sys::random_bytes(&mut key); + key +} + +/// Encrypt plaintext with a random nonce. Returns `nonce || ciphertext`. +pub fn encrypt(key: &[u8; KEY_SIZE], plaintext: &[u8]) -> Vec { + let cipher = XChaCha20Poly1305::new(key.into()); + let mut nonce_bytes = [0u8; NONCE_SIZE]; + tesseras_dht::sys::random_bytes(&mut nonce_bytes); + let nonce = XNonce::from(nonce_bytes); + let ciphertext = cipher + .encrypt(&nonce, plaintext) + .expect("encryption should not fail"); + let mut out = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + out +} + +/// Decrypt `nonce || ciphertext`. Returns `None` if authentication fails. +pub fn decrypt(key: &[u8; KEY_SIZE], data: &[u8]) -> Option> { + if data.len() < NONCE_SIZE { + return None; + } + let (nonce_bytes, ciphertext) = data.split_at(NONCE_SIZE); + let nonce = XNonce::from_slice(nonce_bytes); + let cipher = XChaCha20Poly1305::new(key.into()); + cipher.decrypt(nonce, ciphertext).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let key = generate_key(); + let sealed = encrypt(&key, b"hello"); + let opened = decrypt(&key, &sealed).unwrap(); + assert_eq!(opened, b"hello"); + } + + #[test] + fn wrong_key_fails() { + let key = generate_key(); + let wrong = generate_key(); + let sealed = encrypt(&key, b"secret"); + assert!(decrypt(&wrong, &sealed).is_none()); + } + + #[test] + fn truncated_fails() { + let key = generate_key(); + assert!(decrypt(&key, &[0u8; 10]).is_none()); + } + + #[test] + fn different_nonces() { + let key = generate_key(); + let a = encrypt(&key, b"same"); + let b = encrypt(&key, b"same"); + assert_ne!(a, b); + } +} -- cgit v1.2.3