// 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 std::path::PathBuf; 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 = load_key_material()?; decode_shared_key(&raw) } fn load_key_material() -> Result { if let Some(raw) = std::env::var("LESAVKA_PASTE_KEY") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) { return Ok(raw); } if let Some(path) = std::env::var("LESAVKA_PASTE_KEY_FILE") .ok() .map(PathBuf::from) .or_else(default_paste_key_path) { let raw = std::fs::read_to_string(&path) .with_context(|| format!("reading paste key file {}", path.display()))?; let trimmed = raw.trim().to_string(); if !trimmed.is_empty() { return Ok(trimmed); } anyhow::bail!("paste key file {} is empty", path.display()); } anyhow::bail!( "LESAVKA_PASTE_KEY not set (or no paste key file present at ~/.config/lesavka/paste-key)" ) } fn default_paste_key_path() -> Option { std::env::var_os("HOME").map(|home| { let mut path = PathBuf::from(home); path.push(".config/lesavka/paste-key"); path }) }