// client/src/paste.rs #![forbid(unsafe_code)] use anyhow::{Context, Result}; use base64::Engine as _; use base64::engine::general_purpose::STANDARD; use chacha20poly1305::aead::{Aead, KeyInit, OsRng, rand_core::RngCore}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use lesavka_common::lesavka::PasteRequest; pub fn build_paste_request(text: &str) -> Result { let max = std::env::var("LESAVKA_PASTE_MAX") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(4096); let text = if text.chars().count() > max { text.chars().take(max).collect::() } else { text.to_string() }; let key = load_key()?; let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut nonce_bytes); let nonce = Nonce::from_slice(&nonce_bytes); let ciphertext = cipher .encrypt(nonce, text.as_bytes()) .map_err(|e| anyhow::anyhow!("paste encrypt failed: {e}"))?; Ok(PasteRequest { nonce: nonce_bytes.to_vec(), data: ciphertext, encrypted: true, }) } fn load_key() -> Result<[u8; 32]> { let raw = std::env::var("LESAVKA_PASTE_KEY") .context("LESAVKA_PASTE_KEY not set (required for PasteText RPC)")?; decode_key(&raw) } fn decode_key(raw: &str) -> Result<[u8; 32]> { let s = raw.trim(); let s = s.strip_prefix("hex:").unwrap_or(s); let bytes = if s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit()) { hex_to_bytes(s)? } else { STANDARD .decode(s.as_bytes()) .context("LESAVKA_PASTE_KEY must be 32-byte base64 or 64-char hex")? }; if bytes.len() != 32 { anyhow::bail!("LESAVKA_PASTE_KEY must decode to 32 bytes"); } let mut out = [0u8; 32]; out.copy_from_slice(&bytes); Ok(out) } fn hex_to_bytes(s: &str) -> Result> { let mut out = Vec::with_capacity(s.len() / 2); let chars: Vec = s.chars().collect(); for i in (0..chars.len()).step_by(2) { let hi = chars[i].to_digit(16).context("hex decode failed")?; let lo = chars[i + 1].to_digit(16).context("hex decode failed")?; out.push(((hi << 4) | lo) as u8); } Ok(out) }