// client/src/paste.rs #![forbid(unsafe_code)] use anyhow::{Context, Result}; use chacha20poly1305::aead::{Aead, KeyInit, OsRng, rand_core::RngCore}; use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use lesavka_common::lesavka::PasteRequest; use lesavka_common::paste::{decode_shared_key, truncate_text}; /// Build an encrypted clipboard request for the server's paste RPC. /// /// Inputs: the raw clipboard text captured on the desktop client. /// Outputs: a `PasteRequest` whose payload is truncated to policy, encrypted, /// and marked as such for the server-side validator. /// Why: the client owns nonce generation and pre-flight truncation so oversized /// clipboard content fails predictably before any RPC is attempted. 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 = truncate_text(text, max); 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_shared_key(&raw) }