aboutsummaryrefslogtreecommitdiffstats
path: root/src/ops.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 02:07:37 -0300
committermurilo ijanc2026-03-25 02:07:37 -0300
commit7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch)
treebc987ece7eb78bb8375de1b20123ecd0f90472ba /src/ops.rs
downloadtesseras-paste-7aff2e1d279a4e442b32f49ca0a0eca065355787.tar.gz
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.
Diffstat (limited to 'src/ops.rs')
-rw-r--r--src/ops.rs160
1 files changed, 160 insertions, 0 deletions
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<Vec<u8>, 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<String, PasteError> {
+ 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<Vec<u8>, 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<Vec<u8>, PasteError> {
+ parse_hash(key)
+}