Encrypt clipboard text and file bytes at the engine with ChaCha20-Poly1305 (nonce‖ciphertext; clipboard base64'd into the text field) using a key derived from the shared account secret. Sealing happens BEFORE any transport, so LAN, the SSE relay, and RTC all carry only ciphertext — the server is reduced to pure signaling (presence/SDP/ICE), never content. RTC stays doubly protected under DTLS. Receive decrypts in the message + file sinks; undecryptable payloads (wrong key) are dropped. Plaintext passthrough when signed out (account=""). Honest caveat (in crypto.rs): the key is sha256(account) and the account is an email — low entropy, so this stops passive eavesdroppers but not someone who knows the account. The real fix is a high-entropy secret (Sign in with Apple `sub` / passphrase) → same from_secret() API. Verified by examples/e2e_demo.rs: B decrypts A's clipboard over LAN; a relayed send carries ciphertext only (plaintext never on the bus). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.5 KiB
Rust
67 lines
2.5 KiB
Rust
//! Proves payload E2E encryption.
|
|
//! cargo run --example e2e_demo -- <room> # round-trip (A→B decrypts)
|
|
//! SOLO=1 cargo run --example e2e_demo -- <room> # one sender, no peer →
|
|
//! relay carries ciphertext
|
|
//! In SOLO mode, subscribe to the room on the server and confirm the secret is
|
|
//! NOT visible (only base64 ciphertext in the text field).
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Noop;
|
|
impl MessageHandler for Noop {
|
|
fn on_message(&self, _m: Message) {}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
|
|
}
|
|
struct Collector(mpsc::Sender<String>);
|
|
impl MessageHandler for Collector {
|
|
fn on_message(&self, m: Message) {
|
|
let _ = self.0.send(m.text);
|
|
}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
|
|
}
|
|
|
|
const SECRET_PLAINTEXT: &str = "TOP-SECRET-PLAINTEXT";
|
|
|
|
fn main() {
|
|
let server = std::env::var("TETHER_SERVER").unwrap_or_else(|_| "https://tether.pecord.io".into());
|
|
let room = std::env::args().nth(1).unwrap_or_else(|| "e2e-demo".into());
|
|
let account = "shared-high-entropy-secret"; // same key on both ends
|
|
|
|
if std::env::var("SOLO").is_ok() {
|
|
// One sender, no peer → not "covered" → the relay carries the message.
|
|
let a = Engine::new(server, room, "solo-a".into(), "macos".into(), "A".into(), account.into());
|
|
a.start(Box::new(Noop));
|
|
std::thread::sleep(Duration::from_secs(3));
|
|
eprintln!("sending '{SECRET_PLAINTEXT}' (encrypted) over the relay…");
|
|
a.send(SECRET_PLAINTEXT.to_string());
|
|
std::thread::sleep(Duration::from_secs(3));
|
|
return;
|
|
}
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let a = Engine::new(server.clone(), room.clone(), "dev-a".into(), "macos".into(), "A".into(), account.into());
|
|
let b = Engine::new(server, room, "dev-b".into(), "ios".into(), "B".into(), account.into());
|
|
a.start(Box::new(Noop));
|
|
b.start(Box::new(Collector(tx)));
|
|
|
|
eprintln!("waiting for link…");
|
|
std::thread::sleep(Duration::from_secs(8));
|
|
a.send(SECRET_PLAINTEXT.to_string());
|
|
|
|
match rx.recv_timeout(Duration::from_secs(6)) {
|
|
Ok(t) if t == SECRET_PLAINTEXT => println!("✅ B decrypted the payload: {t:?}"),
|
|
Ok(t) => println!("⚠️ B got {t:?} (expected plaintext)"),
|
|
Err(_) => {
|
|
println!("❌ B got nothing");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
a.stop();
|
|
b.stop();
|
|
}
|