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/ops.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/ops.rs (limited to 'src/ops.rs') diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..302bd58 --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,160 @@ +//! High-level paste operations. +//! +//! Each function combines local storage and DHT interaction +//! into a single call: put, get, delete, pin/unpin. + +use std::time::Duration; + +use tesseras_dht::Node; + +use crate::base58; +use crate::crypto; +use crate::paste::{MAX_PASTE_SIZE, Paste}; +use crate::store::PasteStore; + +/// Timeout for blocking DHT lookups. +const OP_TIMEOUT: Duration = Duration::from_secs(30); + +/// Errors from paste operations. +#[derive(Debug)] +pub enum PasteError { + InvalidKey, + NotFound, + Expired, + DecryptionFailed, + TooLarge, + Internal(String), +} + +impl std::fmt::Display for PasteError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidKey => write!(f, "invalid key"), + Self::NotFound => write!(f, "not found"), + Self::Expired => write!(f, "expired"), + Self::DecryptionFailed => write!(f, "decryption failed"), + Self::TooLarge => write!(f, "paste too large"), + Self::Internal(msg) => write!(f, "internal: {msg}"), + } + } +} + +/// Decode the hash portion of a key string ("hash#enckey" or "hash"). +/// Returns the 32-byte hash. +fn parse_hash(key_str: &str) -> Result, PasteError> { + let hash_b58 = key_str.split_once('#').map(|(h, _)| h).unwrap_or(key_str); + let hash = base58::decode(hash_b58).ok_or(PasteError::InvalidKey)?; + if hash.len() != 32 { + return Err(PasteError::InvalidKey); + } + Ok(hash) +} + +/// Store a paste. If `encrypt` is true, encrypts the content and +/// returns "hash#enckey" in base58. Otherwise returns just the hash. +pub fn put_paste( + node: &mut Node, + store: &PasteStore, + content: &[u8], + ttl_secs: u64, + encrypt: bool, +) -> Result { + if content.len() > MAX_PASTE_SIZE { + return Err(PasteError::TooLarge); + } + + let (paste_content, enc_key) = if encrypt { + let key = crypto::generate_key(); + (crypto::encrypt(&key, content), Some(key)) + } else { + (content.to_vec(), None) + }; + + let paste = Paste::new(paste_content, ttl_secs); + let serialized = paste.to_bytes(); + let hash = Paste::content_key(&paste.content); + + store + .put_paste(&hash, &serialized) + .map_err(|e| PasteError::Internal(e.to_string()))?; + + let dht_ttl = std::cmp::min(ttl_secs, u16::MAX as u64) as u16; + node.put(&hash, &serialized, dht_ttl, false); + + let hash_b58 = base58::encode(&hash); + let label = if encrypt { "encrypted" } else { "public" }; + log::info!( + "put: stored {label} paste {hash_b58} ({} bytes)", + content.len() + ); + + match enc_key { + Some(key) => Ok(format!("{hash_b58}#{}", base58::encode(&key))), + None => Ok(hash_b58), + } +} + +/// Retrieve a paste by key ("hash#enckey" or bare "hash"). +/// Tries local store first, then falls back to a blocking DHT lookup. +pub fn get_paste( + node: &mut Node, + store: &PasteStore, + key_str: &str, +) -> Result, PasteError> { + let (hash_b58, enc_key_b58) = match key_str.split_once('#') { + Some((h, k)) => (h, Some(k)), + None => (key_str, None), + }; + + let hash = base58::decode(hash_b58).ok_or(PasteError::InvalidKey)?; + if hash.len() != 32 { + return Err(PasteError::InvalidKey); + } + + let data = if let Some(local) = store.get_paste(&hash) { + local + } else { + let vals = node.get_blocking(&hash, OP_TIMEOUT); + if vals.is_empty() { + return Err(PasteError::NotFound); + } + vals[0].clone() + }; + + let paste = Paste::from_bytes(&data).ok_or(PasteError::InvalidKey)?; + if paste.is_expired() && !store.is_pinned(&hash) { + return Err(PasteError::Expired); + } + + if let Some(kb58) = enc_key_b58 { + let key_bytes = base58::decode(kb58).ok_or(PasteError::InvalidKey)?; + if key_bytes.len() != crypto::KEY_SIZE { + return Err(PasteError::InvalidKey); + } + let mut key = [0u8; crypto::KEY_SIZE]; + key.copy_from_slice(&key_bytes); + crypto::decrypt(&key, &paste.content) + .ok_or(PasteError::DecryptionFailed) + } else { + Ok(paste.content) + } +} + +/// Delete a paste from local store and the DHT. +pub fn delete_paste( + node: &mut Node, + store: &PasteStore, + key_str: &str, +) -> Result<(), PasteError> { + let hash = parse_hash(key_str)?; + store.remove_paste(&hash); + node.delete(&hash); + let hash_b58 = key_str.split_once('#').map(|(h, _)| h).unwrap_or(key_str); + log::info!("del: removed paste {hash_b58}"); + Ok(()) +} + +/// Parse a key and resolve the 32-byte hash (stripping any #enckey). +pub fn resolve_hash(key: &str) -> Result, PasteError> { + parse_hash(key) +} -- cgit v1.2.3