Three peers join the SAME room → SAME gossip topic, so all three receive the same broadcast bytes off the wire: A sends, B (same account → same key) must decrypt, C (wrong account → wrong key) must NOT. If C — sitting in the identical topic, handed the same ciphertext — can't read what B can, the payload is sealed end-to-end, not merely relayed. Documents/locks in the E2E property of the iroh transport (ChaCha20-Poly1305 seal before gossip; rendezvous + topic membership grant no content access). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.4 KiB
Rust
84 lines
3.4 KiB
Rust
//! Proves the clipboard is genuinely END-TO-END encrypted over iroh.
|
|
//! TETHER_RENDEZVOUS=http://localhost:8765 cargo run --features iroh-transport --example iroh_e2e
|
|
//!
|
|
//! Three peers join the SAME room → SAME gossip topic, so all three receive the
|
|
//! same broadcast bytes off the wire:
|
|
//! A (sender) — account "shared" → key K1
|
|
//! B (right key) — account "shared" → key K1 → MUST decrypt
|
|
//! C (WRONG key) — account "wrong" → key K2 → MUST NOT decrypt
|
|
//! The room is derived from nothing here (passed explicitly), so B and C are in
|
|
//! the identical topic and C truly sees the ciphertext — it just can't open it.
|
|
//! If C can't read what A sent but B can, the payload is E2E-sealed, not merely
|
|
//! relayed in the clear.
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::Duration;
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Tap(&'static str, mpsc::Sender<(&'static str, String)>);
|
|
impl MessageHandler for Tap {
|
|
fn on_message(&self, m: Message) {
|
|
let _ = self.1.send((self.0, m.text));
|
|
}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
|
|
}
|
|
|
|
fn main() {
|
|
let server = std::env::var("TETHER_RENDEZVOUS").unwrap_or_else(|_| "http://localhost:8765".into());
|
|
let room = "e2e-verify";
|
|
let secret = "TOP-SECRET-CLIPBOARD-🔒";
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
// A = sender (opens the topic).
|
|
let a = Engine::new(server.clone(), room.into(), "peer-a".into(), "macos".into(), "A".into(), "shared-key".into());
|
|
a.start(Box::new(Tap("A", tx.clone())));
|
|
std::thread::sleep(Duration::from_secs(4)); // let A register as the bootstrap
|
|
|
|
// B = same account → same key (should decrypt). C = wrong account → wrong key.
|
|
let b = Engine::new(server.clone(), room.into(), "peer-b".into(), "ios".into(), "B".into(), "shared-key".into());
|
|
let c = Engine::new(server, room.into(), "peer-c".into(), "android".into(), "C".into(), "wrong-key".into());
|
|
b.start(Box::new(Tap("B", tx.clone())));
|
|
c.start(Box::new(Tap("C", tx)));
|
|
|
|
eprintln!("waiting for the gossip mesh to form…");
|
|
std::thread::sleep(Duration::from_secs(8));
|
|
eprintln!("A broadcasting (sealed): {secret:?}");
|
|
a.send(secret.to_string());
|
|
|
|
// Collect for a few seconds.
|
|
let mut b_got: Option<String> = None;
|
|
let mut c_got: Option<String> = None;
|
|
let deadline = std::time::Instant::now() + Duration::from_secs(8);
|
|
while std::time::Instant::now() < deadline {
|
|
if let Ok((who, text)) = rx.recv_timeout(Duration::from_millis(500)) {
|
|
match who {
|
|
"B" => b_got = Some(text),
|
|
"C" => c_got = Some(text),
|
|
_ => {}
|
|
}
|
|
}
|
|
if b_got.is_some() && c_got.is_some() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
println!("\n── results ──");
|
|
println!("B (right key): {:?}", b_got);
|
|
println!("C (WRONG key): {:?}", c_got);
|
|
let b_ok = b_got.as_deref() == Some(secret);
|
|
let c_blind = c_got.is_none();
|
|
if b_ok && c_blind {
|
|
println!("\n✅ E2E PROVEN: B (shared key) decrypted the clipboard; C — in the SAME gossip");
|
|
println!(" topic, receiving the same bytes — could NOT read it (wrong key). The payload");
|
|
println!(" is sealed end-to-end, not just relayed.");
|
|
} else {
|
|
println!("\n❌ E2E check failed: b_ok={b_ok} c_blind={c_blind}");
|
|
std::process::exit(1);
|
|
}
|
|
a.stop();
|
|
b.stop();
|
|
c.stop();
|
|
}
|