//! The six operations, AUTH session setup, chain walking, token sync and the //! send/fetch pipelines. Everything here is carrier-neutral: it speaks //! through the `Transport` trait and the bodies are byte-identical on both //! carriers (RNS.md sec 13.5, sec 15). The operations that take a `Store` //! need the `store` feature; `resolve`, `walk_chain` and `accept_mac` are //! always built. use crate::account::RotationCert; use crate::crypto::{ct_eq, hmac_sha256, KEY_LEN}; use crate::error::{expect_ok, Error}; use crate::transport::{ Reader, Transport, CERT_LEN, ID_LEN, LABEL_MAC, MAX_CHAIN, OP_RESOLVE, TOKEN_LEN, }; #[cfg(feature = "store")] use crate::account::{Account, Identity}; #[cfg(feature = "store")] use std::collections::BTreeMap; #[cfg(feature = "store")] use crate::address::{Address, Scheme}; #[cfg(feature = "store")] use crate::crypto::b32; #[cfg(feature = "store")] use crate::message::{ accept_field, build_frontmatter, message_id, parse_frontmatter, seal, unseal, Opened, }; #[cfg(feature = "store")] use crate::store::{now, Store, Stored}; #[cfg(feature = "store")] use crate::tcp::TcpTransport; #[cfg(feature = "store")] use crate::transport::{ FLAG_REQUESTS, LABEL_AUTH, LABEL_REGISTER, OP_AUTH, OP_DELETE, OP_FETCH, OP_REGISTER, OP_SEND, }; /// A connection plus what the caller needs to know about how it was made: /// the server's static key when no pin was checked, for the sec 4 warning. #[cfg(feature = "store")] pub struct Session { transport: Box, pub unpinned_static: Option<[u8; KEY_LEN]>, } #[cfg(feature = "store")] impl Session { pub fn transport(&mut self) -> &mut dyn Transport { self.transport.as_mut() } pub fn close(&mut self) { self.transport.close(); } } /// The account address with the host's dial hint applied, when the caller /// resolved DNS itself: account-bound operations re-read the account, so /// the hint rides along this way rather than in the stored row. #[cfg(feature = "store")] fn account_with_dial(store: &Store, dial: Option<&str>) -> Result { let addr = store .account()? .ok_or(Error::NotRegistered)?; Ok(match dial { Some(dial) => addr.with_dial(dial), None => addr, }) } /// Opens a session, enforcing sec 4's rule about unpinned servers. TCP pins /// the server's Noise static key; on RNS the destination hash is the pin /// (RNS.md sec 13.4). #[cfg(feature = "store")] pub fn connect( store: &Store, addr: &Address, require_pin: bool, timeout: u64, ) -> Result { match addr.scheme { Scheme::Tcp => { let pinned = store.server_pin(&addr.host, addr.port)?; if pinned.is_none() && require_pin { return Err(Error::NotPinned { host: addr.host.clone(), port: addr.port, }); } // The dial hint lets a host that resolves DNS itself (an // Android app) reach the server by IP while `host` keeps its // identity roles. let transport = TcpTransport::connect( addr.dial.as_deref().unwrap_or(&addr.host), addr.port, pinned, timeout, )?; // Pin enforcement lives here, one layer above the transport: the // identity host names the trust object even when a dial hint // routed the packets elsewhere (sec 4). if let Some(pinned) = pinned { if !ct_eq(&pinned, transport.server_static()) { return Err(Error::PinMismatch { host: addr.host.clone(), port: addr.port, pinned, presented: *transport.server_static(), }); } } let unpinned_static = if pinned.is_none() { Some(*transport.server_static()) } else { None }; Ok(Session { transport: Box::new(transport), unpinned_static, }) } Scheme::Rns => { // There is nothing to pin on this carrier (upstream spec 13.4): // the destination hash is the pin, so `require_pin` decides // nothing here and every session is as authenticated as a // pinned TCP one. #[cfg(feature = "rns")] { let _ = require_pin; let destination = addr.destination()?; let transport = crate::rns::transport::RnsTransport::connect( &destination, std::time::Duration::from_secs(timeout), )?; Ok(Session { transport: Box::new(transport), unpinned_static: None, }) } #[cfg(not(feature = "rns"))] { let _ = (addr, require_pin); Err(Error::Other( "smol+rns:// addresses need the rns feature; rebuild with --features rns".into(), )) } } } } fn string_field(out: &mut Vec, text: &[u8]) -> Result<(), Error> { if text.len() > 255 { return Err(Error::Other("field longer than one length byte".into())); } out.push(text.len() as u8); out.extend_from_slice(text); Ok(()) } /// RESOLVE: the username's current key and its rotation chain (sec 6.1). pub fn resolve( transport: &mut dyn Transport, username: &str, ) -> Result<([u8; KEY_LEN], Vec<[u8; CERT_LEN]>), Error> { let mut body = Vec::new(); string_field(&mut body, username.as_bytes())?; let response = transport.request(OP_RESOLVE, &body)?; expect_ok(response.status, &format!("resolving {username}"))?; let mut r = Reader::new(&response.body); let identity: [u8; KEY_LEN] = r.take(KEY_LEN)?.try_into().unwrap(); let chain_len = r.u8()? as usize; let mut chain = Vec::with_capacity(chain_len); for _ in 0..chain_len { chain.push(r.take(CERT_LEN)?.try_into().unwrap()); } r.done()?; Ok((identity, chain)) } /// Accepts a key change only when a signed chain leads from the key we hold /// to the one the server now returns (sec 7). Both keys must sign each link; /// a link predating the key we hold is skipped, a gap is not. pub fn walk_chain( username: &str, pinned: &[u8; KEY_LEN], current: &[u8; KEY_LEN], chain: &[[u8; CERT_LEN]], ) -> bool { if ct_eq(pinned, current) { return true; } if chain.is_empty() || chain.len() > MAX_CHAIN { return false; } let mut key: Vec = pinned.to_vec(); let mut started = false; for cert_bytes in chain { let cert = match RotationCert::from_bytes(cert_bytes) { Ok(cert) => cert, Err(_) => return false, }; if !started { if key != cert.old_pub { continue; // a link predating the key we hold } started = true; } else if key != cert.old_pub { return false; // the chain is not continuous } if !cert.verify(username) { return false; } key = cert.new_pub.to_vec(); } started && ct_eq(&key, current) } /// How `trust_key` left the contact. #[derive(PartialEq, Eq, Debug)] pub enum TrustChange { /// Trust on first use; `unverified` marks a session with no pin (sec 8). New { unverified: bool }, /// A signed chain confirmed a key change (sec 7). Rotated, /// The key matches what we already hold. None, } /// Resolves a contact and applies sec 8's trust rules. A key change without /// a valid chain is an error here: the user confirms out of band and /// imports, rather than clicking through. #[cfg(feature = "store")] pub fn trust_key( store: &Store, transport: &mut dyn Transport, addr: &Address, ) -> Result<([u8; KEY_LEN], TrustChange), Error> { let (identity, chain) = resolve(transport, &addr.user)?; let known = store.contact(&addr.short())?; match known { None => { store.save_contact(&addr.short(), &identity, false)?; Ok(( identity, TrustChange::New { unverified: !transport.pinned(), }, )) } Some((old, _)) if ct_eq(&old, &identity) => Ok((identity, TrustChange::None)), Some((old, verified)) => { if walk_chain(&addr.user, &old, &identity, &chain) { // The displaced key is the only local record that this // contact rotated, so it is kept (sec 8) before the current // one moves on. `until` is milliseconds, the unit the export // format carries. store.save_history(&addr.short(), &old, crate::store::now() * 1000)?; store.save_contact(&addr.short(), &identity, verified)?; Ok((identity, TrustChange::Rotated)) } else { Err(Error::KeyChanged { address: addr.short(), known: old, offered: identity, }) } } } } /// AUTH (sec 4): sign the handshake hash and push the accept-token set. The /// trailing block carries the mailbox's tokens (sec 5.8): `sync = 0` leaves /// the stored set untouched, `sync = 1` replaces it with exactly what /// follows. #[cfg(feature = "store")] pub fn authenticate( transport: &mut dyn Transport, username: &str, account: &Account, store: &Store, ) -> Result { let (sync, tokens) = store.token_set(account)?; let mut body = Vec::new(); string_field(&mut body, username.as_bytes())?; body.extend_from_slice(&account.me().pk()); body.extend_from_slice( &account .me() .sign(&[LABEL_AUTH, &transport.bind().h[..]].concat()), ); body.push(sync); body.extend_from_slice(&(tokens.len() as u16).to_be_bytes()); for token in &tokens { body.extend_from_slice(token); } let response = transport.request(OP_AUTH, &body)?; expect_ok(response.status, "authentication")?; let held = if response.body.len() >= 2 { u16::from_be_bytes(response.body[..2].try_into().unwrap()) } else { 0 }; Ok(held) } /// What pushing the accept-token set achieved (sec 5.8). #[cfg(feature = "store")] pub enum Pushed { /// Not registered yet; the set travels with the first fetch instead. NotRegistered, /// The server now holds this many tokens. Held(u16), } /// An accept or a block only takes effect once the server holds the changed /// set, so it is pushed now rather than at the next fetch. #[cfg(feature = "store")] pub fn push_tokens( store: &Store, account: &Account, dial: Option<&str>, timeout: u64, ) -> Result { let addr = match account_with_dial(store, dial) { Ok(addr) => addr, Err(Error::NotRegistered) => return Ok(Pushed::NotRegistered), Err(e) => return Err(e), }; let mut session = connect(store, &addr, true, timeout)?; let held = authenticate(session.transport(), &addr.user, account, store)?; session.close(); Ok(Pushed::Held(held)) } /// REGISTER (sec 6.1): binds this identity to a username, with proof of /// possession over the server's static key so the attestation cannot be /// replayed to another server. #[cfg(feature = "store")] pub fn register( store: &Store, addr: &Address, account: &Account, invite: Option<&str>, timeout: u64, ) -> Result<(), Error> { let mut session = connect(store, addr, true, timeout)?; let transport = session.transport(); let me = account.me(); let mut body = Vec::new(); string_field(&mut body, addr.user.as_bytes())?; body.extend_from_slice(&me.pk()); body.extend_from_slice( &me.sign( &[ LABEL_REGISTER, &transport.bind().server_static[..], addr.user.as_bytes(), &me.pk()[..], ] .concat(), ), ); let token = invite.unwrap_or("").as_bytes(); string_field(&mut body, token)?; body.push(0); // no rotation certificate on a plain registration let response = transport.request(OP_REGISTER, &body)?; expect_ok(response.status, &format!("registering {}", addr.short()))?; session.close(); store.set_account(addr)?; Ok(()) } /// A completed rotation: the new key and the certificate the server now /// holds, ready to be pushed to contacts as an ordinary message (sec 7). #[cfg(feature = "store")] pub struct Rotated { pub new_pk: [u8; KEY_LEN], pub cert: [u8; CERT_LEN], } /// Advances the rotation index and pushes the certificate (sec 7). The master /// is untouched; only the index moves, and the superseded key stays /// derivable from it (sec 2). #[cfg(feature = "store")] pub fn rotate( store: &Store, account: &Account, dial: Option<&str>, timeout: u64, ) -> Result { let addr = account_with_dial(store, dial)?; if account.index() as usize >= MAX_CHAIN { return Err(Error::ChainLimit { index: account.index() + 1, }); } let old = account.me(); let new = Identity::from_seed(crate::account::identity_seed( account.master(), account.index() + 1, )); let cert = RotationCert::build(old, &new, &addr.user, now()); let mut session = connect(store, &addr, true, timeout)?; let transport = session.transport(); let mut body = Vec::new(); string_field(&mut body, addr.user.as_bytes())?; body.extend_from_slice(&new.pk()); body.extend_from_slice( &new.sign( &[ LABEL_REGISTER, &transport.bind().server_static[..], addr.user.as_bytes(), &new.pk()[..], ] .concat(), ), ); body.push(0); // no invite token on a rotation body.push(CERT_LEN as u8); body.extend_from_slice(&cert); let response = transport.request(OP_REGISTER, &body)?; expect_ok(response.status, "rotating")?; session.close(); store.set_rotations(account.index() + 1)?; Ok(Rotated { new_pk: new.pk(), cert, }) } /// Recovers the rotation index, and with it every superseded key, from the /// master alone (sec 2). The accepted set is gone, so sync is disabled until /// the user rebuilds it: an empty set must not replace the server's (sec 4). /// The pieces are public for hosts that want intermediate UI states: /// `connect` + `resolve`, `find_rotation_index`, then the four `set_*` /// calls below. #[cfg(feature = "store")] pub fn restore(store: &Store, master: &[u8; KEY_LEN], addr: &Address, timeout: u64) -> Result { let mut session = connect(store, addr, false, timeout)?; let (identity, _) = resolve(session.transport(), &addr.user)?; session.close(); let Some(index) = crate::account::find_rotation_index(master, &identity) else { return Err(Error::Other(format!( "the key bound to {} is not derived from this master within {MAX_CHAIN} rotations", addr.short() ))); }; store.set_account(addr)?; store.set_cursor(0, &[0u8; ID_LEN])?; store.set_rotations(index)?; store.set_sync_ok(false)?; Ok(index) } /// One message to send, as the CLI assembled it. #[cfg(feature = "store")] pub struct SendDraft<'a> { pub address: &'a Address, pub text: String, pub subject: Option<&'a str>, /// An id this message replies to, as 64 hex characters. pub reply_to: Option<&'a str>, /// Extra `Key: value` frontmatter fields. pub headers: &'a [(&'a str, &'a str)], /// Omit the Reply-To field carrying this address (sec 5.7). pub anonymous: bool, pub no_pad: bool, } /// What `send` did, for the caller to report. #[cfg(feature = "store")] pub struct Sent { pub id: [u8; ID_LEN], pub bytes: usize, /// An accept token for their mailbox was attached (sec 5.8). pub token_used: bool, /// How the recipient key was obtained, if it was learned now. pub change: TrustChange, /// A non-fatal oddity worth showing the user. pub warning: Option, } /// Seals and delivers one message (sec 5, sec 6.1). Recipient selection /// prefers a key we already trust: a self-certifying address, then a stored /// contact, and only then RESOLVE with trust on first use. #[cfg(feature = "store")] pub fn send( store: &Store, account: &Account, draft: &SendDraft<'_>, timeout: u64, ) -> Result { let addr = draft.address; let me = account.me(); let (recipient, change) = if let Some(key) = addr.identity { store.save_contact(&addr.short(), &key, true)?; (key, TrustChange::None) } else if let Some((known, _)) = store.contact(&addr.short())? { (known, TrustChange::None) } else { let mut session = connect(store, addr, false, timeout)?; let (key, change) = trust_key(store, session.transport(), addr)?; session.close(); (key, change) }; // Frontmatter, in the order the reference client emits it. let mut fields: Vec<(String, String)> = Vec::new(); if let Some(subject) = draft.subject { fields.push(("Subject".into(), subject.to_string())); } if let Some(reply_to) = draft.reply_to { fields.push(("In-Reply-To".into(), reply_to.to_string())); } for (key, value) in draft.headers { fields.push(((*key).to_string(), (*value).to_string())); } // sec 5.7: a signed reply address lets a first-time recipient answer us. if let Some(home) = store.account()? { if !draft.anonymous { fields.push(("Reply-To".into(), home.uri(&me.pk()))); } } // sec 5.8: hand an accepted correspondent the token for our own mailbox. if let Some(identity) = store.accepted_identity(&addr.short())? { fields.push(("Accept".into(), b32(&account.token_for(&identity)))); } let borrowed: Vec<(&str, &str)> = fields .iter() .map(|(k, v)| (k.as_str(), v.as_str())) .collect(); let body = build_frontmatter(&borrowed, &draft.text).into_bytes(); let envelope = seal(me, &recipient, &body, now(), !draft.no_pad)?; let mid = message_id(&envelope); // sec 5.6: the sent copy is sealed to ourselves, not to the recipient — // the recipient's envelope uses an ephemeral key that is gone, so the // only envelope its sender can ever open again is one sealed to their // own long-term key. The stored id stays the delivered message's. let self_copy = seal(me, &me.pk(), &body, now(), !draft.no_pad)?; // sec 5.8: our token for their mailbox, if they have given us one. let mac = match store.token_of(&addr.short())? { Some(token) => hmac_sha256(&token, &[LABEL_MAC, &mid[..]].concat()).to_vec(), None => Vec::new(), }; let mut session = connect(store, addr, false, timeout)?; let mut wire = vec![mac.len() as u8]; wire.extend_from_slice(&mac); wire.extend_from_slice(&envelope); let response = session.transport().request(OP_SEND, &wire)?; expect_ok(response.status, &format!("sending to {}", addr.short()))?; session.close(); // The server echoes the id; it is derived, so ours is authoritative. let warning = if !response.body.is_empty() && response.body != mid.to_vec() { Some("server returned an id we did not derive; it is not authoritative".to_string()) } else { None }; // sec 5.6: the ephemeral is gone, so keep a copy sealed to ourselves. store.store_sent(&mid, &addr.short(), &self_copy, now())?; Ok(Sent { id: mid, bytes: envelope.len(), token_used: !mac.is_empty(), change, warning, }) } /// One rejected fetch record: the id and why it did not enter the inbox. #[cfg(feature = "store")] pub struct Rejected { pub id: [u8; ID_LEN], pub reason: String, } /// What `fetch` did, for the caller to report. #[cfg(feature = "store")] pub struct Fetched { pub total: u32, pub stored: u32, pub rejected: Vec, /// Messages were deleted from the server rather than kept behind a /// cursor (the default). pub acknowledged: bool, /// `cancel` was raised mid-fetch; the summary covers what completed. /// Every envelope the summary counted is accounted for locally, so /// fetching again continues rather than repeats. pub cancelled: bool, } /// The knobs a host with a UI needs on `fetch_with`. Progress needs no /// callback: each envelope is committed to the `Store` as it is verified, so /// a concurrent reader (the store is `Send + Sync`) can list incrementally. #[cfg(feature = "store")] pub struct FetchOptions<'a> { /// Remember the cursor instead of acknowledging with DELETE. pub keep: bool, /// Forget the cursor and page again. pub reset: bool, /// Network timeout in seconds. pub timeout: u64, /// Dial this address instead of the account host, for hosts that /// resolve DNS themselves; the host keeps its identity roles. pub dial: Option<&'a str>, /// Checked between pages and before each envelope; when raised, /// `fetch_with` stops and returns the partial summary with `cancelled` /// set. What was processed is accounted for locally — cursor moved or /// messages deleted — so fetching again continues rather than repeats. pub cancel: Option<&'a std::sync::atomic::AtomicBool>, } #[cfg(feature = "store")] impl FetchOptions<'_> { /// The defaults a plain fetch uses. fn new(keep: bool, reset: bool, timeout: u64) -> Self { FetchOptions { keep, reset, timeout, dial: None, cancel: None, } } } /// Retrieves, verifies and stores mail (sec 6.1), paging forward by /// `(received_at, id)` until a response is empty. Acknowledging deletes what /// it takes, so it always pages from the start; `keep` instead remembers the /// cursor. A message that fails verification is left on the server, so a /// client-side bug cannot lose mail. #[cfg(feature = "store")] pub fn fetch( store: &Store, account: &Account, keep: bool, reset: bool, timeout: u64, ) -> Result { fetch_with(store, account, FetchOptions::new(keep, reset, timeout)) } /// `fetch` with the full option set, for hosts that need cancellation. #[cfg(feature = "store")] pub fn fetch_with( store: &Store, account: &Account, opts: FetchOptions<'_>, ) -> Result { let addr = account_with_dial(store, opts.dial)?; if opts.reset { store.set_cursor(0, &[0u8; ID_LEN])?; } let (mut after_time, mut after_id) = if opts.keep { store.cursor()? } else { (0, [0u8; ID_LEN]) }; let mut summary = Fetched { total: 0, stored: 0, rejected: Vec::new(), acknowledged: !opts.keep, cancelled: false, }; let cancelled = || opts.cancel.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed)); let mut session = connect(store, &addr, true, opts.timeout)?; let transport = session.transport(); authenticate(transport, &addr.user, account, store)?; loop { if cancelled() { summary.cancelled = true; break; } let mut body = Vec::with_capacity(8 + ID_LEN); body.extend_from_slice(&after_time.to_be_bytes()); body.extend_from_slice(&after_id); let response = transport.request(OP_FETCH, &body)?; expect_ok(response.status, "fetching")?; let mut r = Reader::new(&response.body); let count = r.u16()? as usize; if count == 0 { break; } let mut acked: Vec<[u8; ID_LEN]> = Vec::new(); let mut page_cancelled = false; for _ in 0..count { // Checked before each envelope as well as between pages, so a // cancel lands within one envelope; the cursor and acks below // then cover exactly the envelopes that were processed. if cancelled() { page_cancelled = true; summary.cancelled = true; break; } let mid: [u8; ID_LEN] = r.take(ID_LEN)?.try_into().unwrap(); let received_at = r.i64()?; let flags = r.u8()?; let envelope_len = r.u32()? as usize; let envelope = r.take(envelope_len)?.to_vec(); after_time = received_at; after_id = mid; summary.total += 1; let opened = match verify_and_open(account, &mid, &envelope) { Ok(opened) => opened, Err(reason) => { summary.rejected.push(Rejected { id: mid, reason: reason.to_string(), }); continue; } }; // A message we have already had once is not stored again, even // if we deleted it locally in the meantime (sec 10). if !store.seen(&mid)? { let requests_tier = flags & FLAG_REQUESTS != 0; store.store_inbox(&mid, &envelope, received_at, requests_tier, opts.keep)?; learn_token(store, &opened)?; summary.stored += 1; } acked.push(mid); } // A cancelled page is not fully parsed, so trailing bytes are // expected and not an error; everything processed is accounted for. if !page_cancelled { r.done()?; } if opts.keep { store.set_cursor(after_time, &after_id)?; } else if !acked.is_empty() { delete_ids(transport, &acked)?; } if page_cancelled { break; } } session.close(); if !opts.keep { store.set_cursor(0, &[0u8; ID_LEN])?; } Ok(summary) } #[cfg(feature = "store")] fn verify_and_open( account: &Account, mid: &[u8; ID_LEN], envelope: &[u8], ) -> Result { if message_id(envelope) != *mid { return Err(Error::Other("id does not match the envelope".into())); } unseal(account.keys(), envelope, now()) } /// Files the accept token a verified payload carried, under the address that /// signed it (sec 5.8). The signature has already been checked by `unseal`, /// so the attribution is the signer's own claim. #[cfg(feature = "store")] fn learn_token(store: &Store, opened: &Opened) -> Result<(), Error> { let text = String::from_utf8_lossy(&opened.body).into_owned(); let (fields, _) = parse_frontmatter(&text); let Some(token) = accept_field(&fields) else { return Ok(()); }; let reply_to = fields.get("reply-to").map(String::as_str); match store.address_of(&opened.sender, reply_to)? { // No address to send to, so no use for a token. None => Ok(()), Some(address) => store.save_token(&address, &token), } } #[cfg(feature = "store")] fn delete_ids(transport: &mut dyn Transport, ids: &[[u8; ID_LEN]]) -> Result { let mut body = Vec::with_capacity(2 + ids.len() * ID_LEN); body.extend_from_slice(&(ids.len() as u16).to_be_bytes()); for id in ids { body.extend_from_slice(id); } let response = transport.request(OP_DELETE, &body)?; expect_ok(response.status, "acknowledging")?; let removed = if response.body.len() >= 2 { u16::from_be_bytes(response.body[..2].try_into().unwrap()) } else { 0 }; Ok(removed) } /// DELETE (sec 6.1) over an authenticated session: remove ids from the /// server explicitly. Unknown ids are not an error. #[cfg(feature = "store")] pub fn delete( store: &Store, account: &Account, ids: &[[u8; ID_LEN]], dial: Option<&str>, timeout: u64, ) -> Result { let addr = account_with_dial(store, dial)?; let mut session = connect(store, &addr, true, timeout)?; let transport = session.transport(); authenticate(transport, &addr.user, account, store)?; let removed = delete_ids(transport, ids)?; session.close(); Ok(removed) } /// An opened, described message for listing and reading. #[cfg(feature = "store")] pub struct Described { pub sender: [u8; KEY_LEN], pub time: i64, pub fields: BTreeMap, pub text: String, /// The contact address we know the signer by, or a fingerprint. pub from: String, pub subject: String, } #[cfg(feature = "store")] impl Described { /// Whether this payload carried its signer's accept token (sec 5.8): /// machinery, not content. pub fn carries_token(&self) -> bool { self.fields.contains_key("accept") } } /// Opens one sealed message and splits its body into frontmatter and text. #[cfg(feature = "store")] pub fn describe(store: &Store, account: &Account, stored: &Stored) -> Result { let opened = unseal(account.keys(), &stored.envelope, now())?; let text = String::from_utf8_lossy(&opened.body).into_owned(); let (fields, body_text) = parse_frontmatter(&text); let from = store .address_of(&opened.sender, fields.get("reply-to").map(String::as_str))? .unwrap_or_else(|| format!("<{}…>", &b32(&opened.sender)[..20])); Ok(Described { sender: opened.sender, time: opened.time, subject: fields.get("subject").cloned().unwrap_or_default(), fields, text: body_text, from, }) } /// The accept MAC a sender attaches to SEND (sec 5.8), exposed for tests. pub fn accept_mac(token: &[u8; TOKEN_LEN], mid: &[u8; ID_LEN]) -> [u8; 32] { hmac_sha256(token, &[LABEL_MAC, mid].concat()) } /// Proof of possession over a server's static key (sec 6.1), for tests. #[cfg(all(test, feature = "store"))] fn register_signed(server_static: &[u8], username: &str, identity: &[u8]) -> Vec { [LABEL_REGISTER, server_static, username.as_bytes(), identity].concat() } #[cfg(test)] mod tests { use super::*; use crate::account::{identity_seed, verify_sig, Account, Identity, RotationCert}; use crate::transport::{TransportBindValues, SIG_LEN}; fn master() -> [u8; 32] { core::array::from_fn(|i| i as u8) } fn identity(n: u32) -> Identity { Identity::from_seed(identity_seed(&master(), n)) } #[test] fn walk_chain_accepts_signed_progressions() { let old = identity(0); let new = identity(1); let cert = RotationCert::build(&old, &new, "alice", 1700000001); assert!(walk_chain("alice", &old.pk(), &old.pk(), &[])); assert!(walk_chain("alice", &old.pk(), &new.pk(), &[cert])); // The username is covered, so the same cert proves nothing for bob. assert!(!walk_chain("bob", &old.pk(), &new.pk(), &[cert])); // No chain, no acceptance. assert!(!walk_chain("alice", &old.pk(), &new.pk(), &[])); } #[test] fn walk_chain_requires_continuity_and_terminates_at_current() { let keys: Vec = (0..4).map(identity).collect(); let cert01 = RotationCert::build(&keys[0], &keys[1], "alice", 1); let cert12 = RotationCert::build(&keys[1], &keys[2], "alice", 2); let cert23 = RotationCert::build(&keys[2], &keys[3], "alice", 3); // A full chain from 0 to 3. assert!(walk_chain( "alice", &keys[0].pk(), &keys[3].pk(), &[cert01, cert12, cert23] )); // Holding key 1: the earlier link is skipped, the rest must continue. assert!(walk_chain( "alice", &keys[1].pk(), &keys[3].pk(), &[cert01, cert12, cert23] )); // A gap between the held key and the chain never validates. assert!(!walk_chain( "alice", &keys[0].pk(), &keys[3].pk(), &[cert12, cert23] )); // The chain must end at the key RESOLVE returned, not merely agree // partway. assert!(!walk_chain( "alice", &keys[0].pk(), &keys[2].pk(), &[cert01, cert12, cert23] )); } #[test] fn walk_chain_enforces_the_length_limit() { let keys: Vec = (0..=16).map(identity).collect(); let certs: Vec<[u8; CERT_LEN]> = (0..16) .map(|n| RotationCert::build(&keys[n], &keys[n + 1], "alice", n as i64)) .collect(); // Sixteen links walk the whole chain from the first key. assert!(walk_chain("alice", &keys[0].pk(), &keys[16].pk(), &certs)); // Seventeen links is over-long and needs user confirmation, whatever // key we hold (sec 7). let mut over = certs.clone(); let extra = identity(17); over.push(RotationCert::build(&keys[16], &extra, "alice", 17)); assert!(!walk_chain("alice", &keys[0].pk(), &extra.pk(), &over)); assert!(!walk_chain("alice", &keys[1].pk(), &extra.pk(), &over)); } #[test] fn accept_mac_matches_the_reference_vector() { // The reference client's own vector: token_for(pk_1) over the id of // the reference envelope (see the message module's vectors). let v = crate::vectors::load(); let account = Account::new(master(), 0).unwrap(); let token = account.token_for(&identity(1).pk()); let mid: [u8; 32] = crate::crypto::unb32(&v.accept_mac.message_id_b32) .unwrap() .try_into() .unwrap(); assert_eq!( data_encoding::HEXLOWER.encode(&accept_mac(&token, &mid)), v.accept_mac.mac_hex ); } #[cfg(feature = "store")] #[test] fn bind_values_feed_the_auth_and_register_signatures() { let bind = TransportBindValues::tcp(&[2u8; 32], &[7u8; 32]).unwrap(); let account = Account::new(master(), 0).unwrap(); let me = account.me(); let auth = me.sign(&[LABEL_AUTH, &bind.h[..]].concat()); let register = me.sign(®ister_signed(&bind.server_static, "alice", &me.pk())); assert_eq!(auth.len(), SIG_LEN); assert!(verify_sig( &me.pk(), &auth, &[LABEL_AUTH, &bind.h[..]].concat() )); assert!(verify_sig( &me.pk(), ®ister, ®ister_signed(&bind.server_static, "alice", &me.pk()) )); } }