133 lines
4.3 KiB
Rust
133 lines
4.3 KiB
Rust
|
|
// 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,
|
||
|
|
}
|
||
|
|
}
|