lesavka/server/src/paste.rs

133 lines
4.3 KiB
Rust
Raw Normal View History

// 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<String> {
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<File>, text: &str) -> Result<()> {
let max = std::env::var("LESAVKA_PASTE_MAX")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(4096);
let delay_ms = std::env::var("LESAVKA_PASTE_DELAY_MS")
.ok()
.and_then(|v| v.parse::<u64>().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<Vec<u8>> {
let mut out = Vec::with_capacity(s.len() / 2);
let chars: Vec<char> = 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,
}
}