use std::io;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use bytes::{Buf, BufMut, BytesMut};
use chacha20poly1305::aead::rand_core::RngCore;
use chacha20poly1305::aead::{Aead, KeyInit, OsRng};
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
use hkdf::Hkdf;
use peaveil::{Connection, ConnectionSide, CoverStrategy, Handshake, Node, NodeConfig, Pea2Pea};
use sha2::Sha256;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf, split};
use tracing_subscriber::EnvFilter;
const PSK: [u8; 32] = [0x42; 32];
const NONCE_LEN: usize = 32;
const CHUNK_NONCE_LEN: usize = 12;
const CHUNK_TAG_LEN: usize = 16;
const LEN_LEN: usize = 4;
#[derive(Clone)]
struct PskHandshake {
inner: Arc<peashape::Node>,
}
impl Pea2Pea for PskHandshake {
fn node(&self) -> &peaveil::pea2pea::Node {
self.inner.p2p()
}
}
impl Handshake for PskHandshake {
const TIMEOUT_MS: u64 = 5_000;
async fn perform_handshake(&self, mut conn: Connection) -> io::Result<Connection> {
let is_initiator = conn.side() == ConnectionSide::Initiator;
let stream = self.take_stream(&mut conn);
let (mut read, mut write) = split(stream);
let mut my_nonce = [0u8; NONCE_LEN];
OsRng.fill_bytes(&mut my_nonce);
let mut peer_nonce = [0u8; NONCE_LEN];
if is_initiator {
write.write_all(&my_nonce).await?;
read.read_exact(&mut peer_nonce).await?;
} else {
read.read_exact(&mut peer_nonce).await?;
write.write_all(&my_nonce).await?;
}
let (salt, info) = if my_nonce <= peer_nonce {
(&my_nonce[..], &peer_nonce[..])
} else {
(&peer_nonce[..], &my_nonce[..])
};
let hk = Hkdf::<Sha256>::new(Some(salt), &PSK);
let mut session_key = [0u8; 32];
hk.expand(info, &mut session_key)
.map_err(|_| io::Error::other("hkdf expand failed"))?;
let cipher = ChaCha20Poly1305::new(Key::from_slice(&session_key));
let wrapped = BiStream {
read: DecStream::new(read, cipher.clone()),
write: EncStream::new(write, cipher),
};
self.return_stream(&mut conn, wrapped);
Ok(conn)
}
}
struct EncStream<W: AsyncWrite + Unpin> {
inner: W,
cipher: ChaCha20Poly1305,
pending: Option<(usize, BytesMut)>,
}
impl<W: AsyncWrite + Unpin> EncStream<W> {
fn new(inner: W, cipher: ChaCha20Poly1305) -> Self {
Self {
inner,
cipher,
pending: None,
}
}
}
impl<W: AsyncWrite + Unpin> AsyncWrite for EncStream<W> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
if let Some((pt_len, ref mut framed)) = this.pending {
while !framed.is_empty() {
match Pin::new(&mut this.inner).poll_write(cx, framed) {
Poll::Ready(Ok(0)) => return Poll::Ready(Ok(0)),
Poll::Ready(Ok(n)) => framed.advance(n),
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
this.pending = None;
return Poll::Ready(Ok(pt_len));
}
if buf.is_empty() {
return Poll::Ready(Ok(0));
}
let mut chunk_nonce = [0u8; CHUNK_NONCE_LEN];
OsRng.fill_bytes(&mut chunk_nonce);
let ct = this
.cipher
.encrypt(Nonce::from_slice(&chunk_nonce), buf)
.map_err(|_| io::Error::other("AEAD encrypt failed"))?;
let mut framed = BytesMut::with_capacity(LEN_LEN + CHUNK_NONCE_LEN + ct.len());
framed.put_u32((CHUNK_NONCE_LEN + ct.len()) as u32);
framed.extend_from_slice(&chunk_nonce);
framed.extend_from_slice(&ct);
let pt_len = buf.len();
while !framed.is_empty() {
match Pin::new(&mut this.inner).poll_write(cx, &framed) {
Poll::Ready(Ok(0)) => {
this.pending = Some((pt_len, framed));
return Poll::Ready(Ok(0));
}
Poll::Ready(Ok(n)) => framed.advance(n),
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => {
this.pending = Some((pt_len, framed));
return Poll::Pending;
}
}
}
Poll::Ready(Ok(pt_len))
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
}
}
struct DecStream<R: AsyncRead + Unpin> {
inner: R,
cipher: ChaCha20Poly1305,
pending: BytesMut,
inbound: BytesMut,
}
impl<R: AsyncRead + Unpin> DecStream<R> {
fn new(inner: R, cipher: ChaCha20Poly1305) -> Self {
Self {
inner,
cipher,
pending: BytesMut::new(),
inbound: BytesMut::new(),
}
}
}
impl<R: AsyncRead + Unpin> AsyncRead for DecStream<R> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
if !self.pending.is_empty() {
let n = std::cmp::min(self.pending.len(), buf.remaining());
buf.put_slice(&self.pending[..n]);
self.pending.advance(n);
return Poll::Ready(Ok(()));
}
loop {
if self.inbound.len() >= LEN_LEN {
let len =
u32::from_be_bytes(self.inbound[..LEN_LEN].try_into().expect("LEN_LEN bytes"))
as usize;
if len < CHUNK_NONCE_LEN + CHUNK_TAG_LEN {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"AEAD chunk shorter than nonce + tag",
)));
}
if self.inbound.len() >= LEN_LEN + len {
self.inbound.advance(LEN_LEN);
let body = self.inbound.split_to(len);
let (chunk_nonce, ct) = body.split_at(CHUNK_NONCE_LEN);
let pt = self
.cipher
.decrypt(Nonce::from_slice(chunk_nonce), ct)
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "AEAD decrypt failed")
})?;
let n = std::cmp::min(pt.len(), buf.remaining());
buf.put_slice(&pt[..n]);
if n < pt.len() {
self.pending = BytesMut::from(&pt[n..]);
}
return Poll::Ready(Ok(()));
}
}
let mut tmp = [0u8; 4096];
let mut tmp_buf = ReadBuf::new(&mut tmp);
match Pin::new(&mut self.inner).poll_read(cx, &mut tmp_buf) {
Poll::Ready(Ok(())) => {
let n = tmp_buf.filled().len();
if n == 0 {
return Poll::Ready(Err(io::ErrorKind::UnexpectedEof.into()));
}
self.inbound.extend_from_slice(&tmp[..n]);
}
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
Poll::Pending => return Poll::Pending,
}
}
}
}
struct BiStream<R, W> {
read: R,
write: W,
}
impl<R: AsyncRead + Unpin, W: AsyncWrite + Unpin> AsyncRead for BiStream<R, W> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.read).poll_read(cx, buf)
}
}
impl<R: Unpin, W: AsyncWrite + Unpin> AsyncWrite for BiStream<R, W> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.get_mut().write).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().write).poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.get_mut().write).poll_shutdown(cx)
}
}
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
println!("╔═════════════════════════════════════════════════════════════╗");
println!("║ peaveil — encrypted (PSK + ChaCha20-Poly1305) demo ║");
println!("╚═════════════════════════════════════════════════════════════╝");
println!();
println!("Two nodes share a 32-byte PSK, perform a 32-byte nonce");
println!("exchange, derive a session key with HKDF-SHA256, and wrap");
println!("the TCP stream in length-prefixed ChaCha20-Poly1305 chunks.");
println!("A passive observer now sees only AEAD ciphertext + cover.\n");
fn mk_node(name: &str) -> Result<Node, Box<dyn std::error::Error>> {
Ok(Node::new(NodeConfig {
name: Some(name.into()),
listener_addr: Some("127.0.0.1:0".parse()?),
cover: CoverStrategy::Constant {
interval: Duration::from_millis(100),
},
..Default::default()
}))
}
let alice = mk_node("alice")?;
let bob = mk_node("bob")?;
for n in [&alice, &bob] {
let hs = PskHandshake {
inner: Arc::new(n.peashape().clone()),
};
hs.enable_handshake().await;
}
alice.spawn().await?;
bob.spawn().await?;
let alice_addr = alice.local_addr().await?.expect("alice bound");
let bob_addr = bob.local_addr().await?.expect("bob bound");
println!("alice bound on {alice_addr}");
println!("bob bound on {bob_addr}");
alice.add_recent(bob_addr);
bob.add_recent(alice_addr);
alice.connect(bob_addr).await?;
bob.connect(alice_addr).await?;
for _ in 0..50 {
if alice.connected_peers().contains(&bob_addr)
&& bob.connected_peers().contains(&alice_addr)
{
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
if !alice.connected_peers().contains(&bob_addr) {
eprintln!("alice did not connect to bob within the deadline");
alice.shutdown().await;
bob.shutdown().await;
std::process::exit(1);
}
let mut alice_events = alice.subscribe_events();
let alice_events_task = tokio::spawn(async move {
while let Ok(event) = alice_events.recv().await {
match event {
peaveil::DiscoveryEvent::SampleSent { to, count } => {
println!("alice -> {to}: sent sample ({count} entries, over AEAD)");
}
peaveil::DiscoveryEvent::SampleReceived { count, .. } => {
println!("alice <- ???: received sample ({count} entries, decrypted)");
}
peaveil::DiscoveryEvent::PeerDiscovered { addr, category } => {
println!("alice discovered {addr} as {category:?}");
}
_ => {}
}
}
});
tokio::time::sleep(Duration::from_secs(3)).await;
alice_events_task.abort();
println!();
println!("=== Alice's view (after the encrypted exchange) ===");
print_view(&alice.view());
println!();
println!("=== Bob's view (after the encrypted exchange) ===");
print_view(&bob.view());
alice.shutdown().await;
bob.shutdown().await;
Ok(())
}
fn print_view(view: &peaveil::ViewSnapshot) {
let total = view.total();
if total == 0 {
println!(" (empty — only self-entry, no other peers known)");
return;
}
for p in &view.trusted {
println!(
" [trusted] {}:{} (seen {} times)",
p.addr.ip(),
p.addr.port(),
p.seen_count,
);
}
for p in &view.recent {
println!(
" [recent] {}:{} (seen {} times)",
p.addr.ip(),
p.addr.port(),
p.seen_count,
);
}
for p in &view.random {
println!(
" [random] {}:{} (seen {} times)",
p.addr.ip(),
p.addr.port(),
p.seen_count,
);
}
for p in &view.bootstrap {
println!(" [boot] {}:{} (bootstrap)", p.addr.ip(), p.addr.port(),);
}
}