lesavka/client/src/paste.rs

83 lines
2.6 KiB
Rust

// 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<PasteRequest> {
let max = std::env::var("LESAVKA_PASTE_MAX")
.ok()
.and_then(|v| v.parse::<usize>().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<String> {
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<PathBuf> {
std::env::var_os("HOME").map(|home| {
let mut path = PathBuf::from(home);
path.push(".config/lesavka/paste-key");
path
})
}