use super::hub::RelayLimits;
use super::{auth_message, ticket_message, CircleId, Class, ClientFrame, MailboxId, ServerFrame};
use crate::remote::util::hex;
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use ring::signature::{Ed25519KeyPair, KeyPair};
use std::collections::{HashSet, VecDeque};
const SEEN_LIMIT: usize = 4096;
const ACK_BATCH: usize = 100;
#[derive(Debug)]
pub struct SealError(pub String);
impl std::fmt::Display for SealError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "seal: {}", self.0)
}
}
impl std::error::Error for SealError {}
pub trait Sealer {
fn seal(&self, to: &MailboxId, plaintext: &[u8]) -> Result<Vec<u8>, SealError>;
fn open(&self, from: &MailboxId, sealed: &[u8]) -> Result<Vec<u8>, SealError>;
}
#[derive(Debug)]
pub enum ClientError {
DirectOnly,
TooLarge {
len: usize,
cap: usize,
},
Seal(SealError),
Random,
KeyMismatch,
Protocol(&'static str),
}
impl std::fmt::Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientError::DirectOnly => {
f.write_str("this class is direct only; the relay refuses it")
}
ClientError::TooLarge { len, cap } => {
write!(f, "sealed message is {len} bytes, the relay cap is {cap}")
}
ClientError::Seal(e) => write!(f, "{e}"),
ClientError::Random => f.write_str("the OS random source failed"),
ClientError::KeyMismatch => f.write_str("the credential is for a different device key"),
ClientError::Protocol(what) => write!(f, "relay protocol: {what}"),
}
}
}
impl std::error::Error for ClientError {}
#[derive(Clone, Debug)]
pub struct DeviceCredential {
pub owner_pub: [u8; 32],
pub device_pub: [u8; 32],
pub exp: u64,
pub ticket_sig: Vec<u8>,
}
fn public_of(pair: &Ed25519KeyPair) -> [u8; 32] {
let mut out = [0u8; 32];
out.copy_from_slice(pair.public_key().as_ref());
out
}
pub fn issue_ticket(owner: &Ed25519KeyPair, device_pub: &[u8; 32], exp: u64) -> DeviceCredential {
let owner_pub = public_of(owner);
let ticket_sig = owner
.sign(&ticket_message(&owner_pub, device_pub, exp))
.as_ref()
.to_vec();
DeviceCredential {
owner_pub,
device_pub: *device_pub,
exp,
ticket_sig,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Delivery {
pub id: [u8; 16],
pub from: MailboxId,
pub class: Class,
pub plaintext: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Event {
Reply(ClientFrame),
Authed,
Message(Delivery),
Duplicate,
Undecryptable {
id: [u8; 16],
},
Stored {
id: [u8; 16],
seq: u64,
},
Acked(u32),
Failed {
code: String,
message: String,
retry_after: Option<u64>,
},
Pong,
}
pub struct RelaySession<S: Sealer> {
key: Ed25519KeyPair,
credential: DeviceCredential,
sealer: S,
limits: RelayLimits,
seen: HashSet<[u8; 16]>,
order: VecDeque<[u8; 16]>,
to_ack: Vec<String>,
}
impl<S: Sealer> RelaySession<S> {
pub fn new(
key: Ed25519KeyPair,
credential: DeviceCredential,
sealer: S,
limits: RelayLimits,
) -> Result<Self, ClientError> {
if public_of(&key) != credential.device_pub {
return Err(ClientError::KeyMismatch);
}
Ok(Self {
key,
credential,
sealer,
limits,
seen: HashSet::new(),
order: VecDeque::new(),
to_ack: Vec::new(),
})
}
pub fn mailbox(&self) -> MailboxId {
MailboxId::of_device(&self.credential.device_pub)
}
pub fn circle(&self) -> CircleId {
CircleId::of_owner(&self.credential.owner_pub)
}
pub fn put(
&self,
to: &MailboxId,
class: Class,
plaintext: &[u8],
) -> Result<(String, ClientFrame), ClientError> {
let cap = self.limits.cap(class).ok_or(ClientError::DirectOnly)?;
let sealed = self.sealer.seal(to, plaintext).map_err(ClientError::Seal)?;
if sealed.len() > cap {
return Err(ClientError::TooLarge {
len: sealed.len(),
cap,
});
}
let mut id = [0u8; 16];
getrandom::fill(&mut id).map_err(|_| ClientError::Random)?;
let id_hex = hex(&id);
Ok((
id_hex.clone(),
ClientFrame::Put {
id: id_hex,
to: to.to_hex(),
class,
body: STANDARD.encode(sealed),
},
))
}
pub fn fetch(&self, limit: u32) -> ClientFrame {
ClientFrame::Fetch { limit }
}
pub fn take_ack(&mut self) -> Option<ClientFrame> {
if self.to_ack.is_empty() {
return None;
}
let take = self.to_ack.len().min(ACK_BATCH);
let ids: Vec<String> = self.to_ack.drain(..take).collect();
Some(ClientFrame::Ack { ids })
}
fn remember(&mut self, id: [u8; 16]) {
if self.seen.insert(id) {
self.order.push_back(id);
if self.order.len() > SEEN_LIMIT {
if let Some(old) = self.order.pop_front() {
self.seen.remove(&old);
}
}
}
}
pub fn handle(&mut self, frame: ServerFrame) -> Result<Event, ClientError> {
match frame {
ServerFrame::Challenge { nonce } => {
let nonce = super::unhex_array::<32>(&nonce)
.ok_or(ClientError::Protocol("malformed challenge"))?;
let proof = self
.key
.sign(&auth_message(&nonce, &self.credential.device_pub));
Ok(Event::Reply(ClientFrame::Auth {
owner_pub: hex(&self.credential.owner_pub),
device_pub: hex(&self.credential.device_pub),
exp: self.credential.exp,
ticket_sig: hex(&self.credential.ticket_sig),
challenge_sig: hex(proof.as_ref()),
}))
}
ServerFrame::Authed { mailbox, circle } => {
if MailboxId::from_hex(&mailbox) != Some(self.mailbox())
|| CircleId::from_hex(&circle) != Some(self.circle())
{
return Err(ClientError::Protocol("relay named a different mailbox"));
}
Ok(Event::Authed)
}
ServerFrame::Stored { id, seq } => {
let id =
super::unhex_array::<16>(&id).ok_or(ClientError::Protocol("malformed id"))?;
Ok(Event::Stored { id, seq })
}
ServerFrame::Deliver {
id,
from,
class,
body,
..
} => {
let id_bytes =
super::unhex_array::<16>(&id).ok_or(ClientError::Protocol("malformed id"))?;
let from =
MailboxId::from_hex(&from).ok_or(ClientError::Protocol("malformed sender"))?;
if self.seen.contains(&id_bytes) {
self.to_ack.push(id);
return Ok(Event::Duplicate);
}
let sealed = STANDARD
.decode(body)
.map_err(|_| ClientError::Protocol("body is not base64"))?;
self.to_ack.push(id);
self.remember(id_bytes);
match self.sealer.open(&from, &sealed) {
Ok(plaintext) => Ok(Event::Message(Delivery {
id: id_bytes,
from,
class,
plaintext,
})),
Err(_) => Ok(Event::Undecryptable { id: id_bytes }),
}
}
ServerFrame::Acked { count } => Ok(Event::Acked(count)),
ServerFrame::Error {
code,
message,
retry_after,
} => Ok(Event::Failed {
code,
message,
retry_after,
}),
ServerFrame::Pong => Ok(Event::Pong),
}
}
}