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/paste.rs | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/paste.rs (limited to 'src/paste.rs') diff --git a/src/paste.rs b/src/paste.rs new file mode 100644 index 0000000..50b32b1 --- /dev/null +++ b/src/paste.rs @@ -0,0 +1,128 @@ +//! Paste record format. +//! +//! Binary format (no external serialization deps): +//! version: u8 +//! created_at: u64 (big-endian) +//! ttl_secs: u64 (big-endian) +//! content: [u8] (remaining bytes) + +use tesseras_dht::sha2::{Digest, Sha256}; + +/// Maximum paste size: 64 KiB. +pub const MAX_PASTE_SIZE: usize = 64 * 1024; + +/// Current format version. +const FORMAT_VERSION: u8 = 1; + +/// Header size: version(1) + created_at(8) + ttl(8) = 17. +const HEADER_SIZE: usize = 17; + +/// A paste record stored locally and replicated via the DHT. +#[derive(Debug, Clone)] +pub struct Paste { + pub version: u8, + pub content: Vec, + pub created_at: u64, + pub ttl_secs: u64, +} + +impl Paste { + /// Create a new paste with the current timestamp. + pub fn new(content: Vec, ttl_secs: u64) -> Self { + let created_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + Paste { + version: FORMAT_VERSION, + content, + created_at, + ttl_secs, + } + } + + /// Compute the SHA-256 content-addressed key (32 bytes). + pub fn content_key(content: &[u8]) -> [u8; 32] { + let mut h = Sha256::new(); + h.update(content); + h.finalize().into() + } + + /// Serialize to bytes. + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(HEADER_SIZE + self.content.len()); + buf.push(self.version); + buf.extend_from_slice(&self.created_at.to_be_bytes()); + buf.extend_from_slice(&self.ttl_secs.to_be_bytes()); + buf.extend_from_slice(&self.content); + buf + } + + /// Deserialize from bytes. + pub fn from_bytes(data: &[u8]) -> Option { + if data.len() < HEADER_SIZE { + return None; + } + let version = data[0]; + let created_at = u64::from_be_bytes(data[1..9].try_into().ok()?); + let ttl_secs = u64::from_be_bytes(data[9..17].try_into().ok()?); + let content = data[HEADER_SIZE..].to_vec(); + Some(Paste { + version, + content, + created_at, + ttl_secs, + }) + } + + /// Whether this paste has expired. + pub fn is_expired(&self) -> bool { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + now > self.created_at + self.ttl_secs + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let paste = Paste::new(b"hello world".to_vec(), 3600); + let bytes = paste.to_bytes(); + let decoded = Paste::from_bytes(&bytes).unwrap(); + assert_eq!(decoded.content, b"hello world"); + assert_eq!(decoded.ttl_secs, 3600); + assert_eq!(decoded.version, FORMAT_VERSION); + } + + #[test] + fn content_key_deterministic() { + let k1 = Paste::content_key(b"test"); + let k2 = Paste::content_key(b"test"); + assert_eq!(k1, k2); + } + + #[test] + fn content_key_differs() { + let k1 = Paste::content_key(b"aaa"); + let k2 = Paste::content_key(b"bbb"); + assert_ne!(k1, k2); + } + + #[test] + fn too_short_fails() { + assert!(Paste::from_bytes(&[0u8; 5]).is_none()); + } + + #[test] + fn empty_content() { + let paste = Paste::new(Vec::new(), 60); + let bytes = paste.to_bytes(); + let decoded = Paste::from_bytes(&bytes).unwrap(); + assert!(decoded.content.is_empty()); + } +} -- cgit v1.2.3