use anyhow::{Context, Result, bail, ensure};
use iroh::{EndpointId, SecretKey, Signature};
use iroh_gossip::proto::TopicId;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(target_arch = "wasm32")]
use web_time::{SystemTime, UNIX_EPOCH};
pub const VERSION: u16 = 1;
pub const MAX_FRAME: usize = 1 << 20;
pub const MAX_ITEMS: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Kind(pub u64);
impl Kind {
pub const fn named(name: &str) -> Kind {
Kind(fnv1a(name.as_bytes()))
}
pub fn of<T: ?Sized>() -> Kind {
Kind::named(std::any::type_name::<T>())
}
pub const HELLO: Kind = Kind::named("bevy_iroh/hello/1");
pub const GOODBYE: Kind = Kind::named("bevy_iroh/goodbye/1");
pub const PING: Kind = Kind::named("bevy_iroh/ping/1");
pub const PONG: Kind = Kind::named("bevy_iroh/pong/1");
}
pub const fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(0x0100_0000_01b3);
i += 1;
}
hash
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Kind::HELLO => f.write_str("hello"),
Kind::GOODBYE => f.write_str("goodbye"),
Kind::PING => f.write_str("ping"),
Kind::PONG => f.write_str("pong"),
_ => write!(f, "kind({:016x})", self.0),
}
}
}
pub trait Payload: Serialize + DeserializeOwned {
const KIND: Kind;
fn to_body(&self) -> Result<Vec<u8>> {
postcard::to_stdvec(self).context("encode payload")
}
fn from_body(body: &[u8]) -> Result<Self> {
postcard::from_bytes(body).context("decode payload")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Audience {
Everyone,
Only(Vec<EndpointId>),
}
impl Audience {
pub fn includes(&self, me: &EndpointId) -> bool {
match self {
Audience::Everyone => true,
Audience::Only(peers) => peers.contains(me),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub version: u16,
pub topic: TopicId,
pub kind: Kind,
pub audience: Audience,
pub sent_at_ms: u64,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Signed {
from: EndpointId,
envelope: Vec<u8>,
signature: Vec<u8>,
}
pub fn encode_raw(
secret: &SecretKey,
topic: TopicId,
audience: Audience,
kind: Kind,
body: Vec<u8>,
) -> Result<Vec<u8>> {
let envelope = postcard::to_stdvec(&Envelope {
version: VERSION,
topic,
kind,
audience,
sent_at_ms: now_ms(),
body,
})
.context("encode envelope")?;
let signature = secret.sign(&envelope);
postcard::to_stdvec(&Signed {
from: secret.public(),
envelope,
signature: signature.to_bytes().to_vec(),
})
.context("encode signed message")
}
pub fn encode<P: Payload>(
secret: &SecretKey,
topic: TopicId,
audience: Audience,
payload: &P,
) -> Result<Vec<u8>> {
encode_raw(secret, topic, audience, P::KIND, payload.to_body()?)
}
pub fn decode(bytes: &[u8]) -> Result<(EndpointId, Envelope)> {
ensure!(
bytes.len() <= MAX_FRAME,
"frame of {} bytes exceeds the {MAX_FRAME} byte limit",
bytes.len()
);
let signed: Signed = postcard::from_bytes(bytes).context("decode signed message")?;
let signature: [u8; Signature::LENGTH] = signed
.signature
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("signature is {} bytes", signed.signature.len()))?;
signed
.from
.verify(&signed.envelope, &Signature::from_bytes(&signature))
.context("bad signature")?;
let envelope: Envelope = postcard::from_bytes(&signed.envelope).context("decode envelope")?;
if envelope.version != VERSION {
bail!(
"peer speaks protocol version {} and we speak {VERSION}",
envelope.version
);
}
Ok((signed.from, envelope))
}
pub fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hello {
pub name: String,
}
impl Payload for Hello {
const KIND: Kind = Kind::HELLO;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Goodbye;
impl Payload for Goodbye {
const KIND: Kind = Kind::GOODBYE;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ping {
pub nonce: u64,
}
impl Payload for Ping {
const KIND: Kind = Kind::PING;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pong {
pub nonce: u64,
pub ping_sent_at_ms: u64,
}
impl Payload for Pong {
const KIND: Kind = Kind::PONG;
}
#[cfg(test)]
mod tests {
use super::*;
fn topic() -> TopicId {
TopicId::from_bytes([7; 32])
}
#[test]
fn round_trips() {
let secret = SecretKey::generate();
let bytes = encode(
&secret,
topic(),
Audience::Everyone,
&Hello { name: "a".into() },
)
.unwrap();
let (from, envelope) = decode(&bytes).unwrap();
assert_eq!(from, secret.public());
assert_eq!(envelope.kind, Kind::HELLO);
assert_eq!(envelope.topic, topic());
assert_eq!(Hello::from_body(&envelope.body).unwrap().name, "a");
}
#[test]
fn rejects_a_forged_signature() {
let secret = SecretKey::generate();
let mut bytes = encode(&secret, topic(), Audience::Everyone, &Ping { nonce: 1 }).unwrap();
let last = bytes.len() - 1;
bytes[last] ^= 0x01;
assert!(decode(&bytes).is_err());
}
#[test]
fn rejects_a_body_edited_in_flight() {
let secret = SecretKey::generate();
let bytes = encode(
&secret,
topic(),
Audience::Everyone,
&Hello {
name: "pay alice".into(),
},
)
.unwrap();
let mut forged = bytes.clone();
let at = forged.windows(5).position(|w| w == b"alice").unwrap();
forged[at] = b'm';
assert!(decode(&forged).is_err());
}
#[test]
fn garbage_is_an_error_not_a_panic() {
assert!(decode(&[]).is_err());
assert!(decode(&[0xff; 64]).is_err());
assert!(decode(&vec![0u8; MAX_FRAME + 1]).is_err());
}
#[test]
fn an_unknown_kind_still_decodes_as_an_envelope() {
#[derive(Serialize, Deserialize)]
struct FromTheFuture {
hash: [u8; 32],
}
impl Payload for FromTheFuture {
const KIND: Kind = Kind::named("someone-else/future/9");
}
let secret = SecretKey::generate();
let bytes = encode(
&secret,
topic(),
Audience::Everyone,
&FromTheFuture { hash: [3; 32] },
)
.unwrap();
let (from, envelope) = decode(&bytes).unwrap();
assert_eq!(from, secret.public());
assert_eq!(envelope.kind, Kind::named("someone-else/future/9"));
assert!(!envelope.body.is_empty());
}
#[test]
fn kinds_are_stable_and_distinct() {
assert_eq!(Kind::named("bevy_iroh/hello/1"), Kind::HELLO);
assert_ne!(Kind::HELLO, Kind::GOODBYE);
assert_eq!(Kind::of::<Hello>(), Kind::of::<Hello>());
assert_ne!(Kind::of::<Hello>(), Kind::of::<Goodbye>());
}
#[test]
fn only_addresses_the_named_peers() {
let alice = SecretKey::generate().public();
let bob = SecretKey::generate().public();
assert!(Audience::Everyone.includes(&alice));
assert!(Audience::Only(vec![alice]).includes(&alice));
assert!(!Audience::Only(vec![alice]).includes(&bob));
}
}