//! 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); } // Verify DHT result: the content hash must match the // requested key to prevent a malicious node from // injecting arbitrary data. match vals.iter().find(|v| { Paste::from_bytes(v) .map(|p| Paste::content_key(&p.content) == *hash) .unwrap_or(false) }) { Some(v) => v.clone(), None => return Err(PasteError::NotFound), } }; 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) }