// server/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}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; use lesavka_common::lesavka::PasteRequest; pub fn decrypt(req: &PasteRequest) -> Result { if !req.encrypted { anyhow::bail!("paste request must be encrypted"); } let key = load_key()?; let cipher = ChaCha20Poly1305::new(Key::from_slice(&key)); let nonce = Nonce::from_slice(&req.nonce); let plaintext = cipher .decrypt(nonce, req.data.as_ref()) .map_err(|e| anyhow::anyhow!("paste decrypt failed: {e}"))?; Ok(String::from_utf8(plaintext).context("paste plaintext not UTF-8")?) } pub async fn type_text(kb: &Mutex, text: &str) -> Result<()> { let max = std::env::var("LESAVKA_PASTE_MAX") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(4096); let delay_ms = std::env::var("LESAVKA_PASTE_DELAY_MS") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(1); let delay = std::time::Duration::from_millis(delay_ms); let mut kb = kb.lock().await; for c in text.chars().take(max) { if let Some((usage, mods)) = char_to_usage(c) { let report = [mods, 0, usage, 0, 0, 0, 0, 0]; kb.write_all(&report).await?; kb.write_all(&[0u8; 8]).await?; if delay_ms > 0 { tokio::time::sleep(delay).await; } } } Ok(()) } 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) } fn char_to_usage(c: char) -> Option<(u8, u8)> { let shift = 0x02; // left shift in HID modifier byte match c { 'a'..='z' => Some((0x04 + (c as u8 - b'a'), 0)), 'A'..='Z' => Some((0x04 + (c as u8 - b'A'), shift)), '1'..='9' => Some((0x1E + (c as u8 - b'1'), 0)), '0' => Some((0x27, 0)), '!' => Some((0x1E, shift)), '@' => Some((0x1F, shift)), '#' => Some((0x20, shift)), '$' => Some((0x21, shift)), '%' => Some((0x22, shift)), '^' => Some((0x23, shift)), '&' => Some((0x24, shift)), '*' => Some((0x25, shift)), '(' => Some((0x26, shift)), ')' => Some((0x27, shift)), '-' => Some((0x2D, 0)), '_' => Some((0x2D, shift)), '=' => Some((0x2E, 0)), '+' => Some((0x2E, shift)), '[' => Some((0x2F, 0)), '{' => Some((0x2F, shift)), ']' => Some((0x30, 0)), '}' => Some((0x30, shift)), '\\' => Some((0x31, 0)), '|' => Some((0x31, shift)), ';' => Some((0x33, 0)), ':' => Some((0x33, shift)), '\'' => Some((0x34, 0)), '"' => Some((0x34, shift)), '`' => Some((0x35, 0)), '~' => Some((0x35, shift)), ',' => Some((0x36, 0)), '<' => Some((0x36, shift)), '.' => Some((0x37, 0)), '>' => Some((0x37, shift)), '/' => Some((0x38, 0)), '?' => Some((0x38, shift)), ' ' => Some((0x2C, 0)), '\n' | '\r' => Some((0x28, 0)), '\t' => Some((0x2B, 0)), _ => None, } }