Mailboxes
A Mailbox is where a device receives envelopes for one group. Every operation here proves ownership of a mailbox secret; none of them identifies as a device, and the server never learns that two mailboxes belong to the same one.
struct DeliveryAddress { // Base32 identifier, 24 chars, derived from mailbox_public prefix: String,
// Server domain (e.g., "chat.example.com") server: String,
// Senders encrypt the outer envelope layer to this key. Reaches contacts // inside MLS via AddressRotation, never via server. mailbox_public: X25519PublicKey,
// Timestamp of when the address was generated created_at: u64,
// Kept live through the rotation overlap window, so a contact who missed an // AddressRotation still reaches a real mailbox active: bool,}
impl DeliveryAddress { fn full_address(&self) -> String { format!("{}@{}", self.prefix, self.server) }
// Anyone holding the address can check if the prefix binds to the key fn verify_binding(&self) -> bool { let hash = blake3::hash(self.mailbox_public.as_bytes()); self.prefix == base32::encode(&hash.as_bytes()[..15]) }}/// Proof of mailbox ownership, produced by signing a server-issued nonce/// with the mailbox secret. Used at registration and at retrieval; the/// server verifies it against the address prefix and nothing elsestruct MailboxProof { // The mailbox this proves ownership of. The server checks that the // address prefix derives from it. mailbox_public: X25519PublicKey,
// Signature over the server's challenge nonce. Never a static value: // a fixed signature would be replayable by anyone who saw it once. signature: Ed25519Signature,}RequestMailboxChallenge
Section titled “RequestMailboxChallenge”Obtains a nonce to sign. Used before registration and before opening a retrieval session.
- Input:
ChallengeRequest { prefix } - Output:
MailboxChallenge { nonce, expires_at } - Auth: None
- Idempotent: No
- Errors:
MALFORMED_REQUEST
Requirements
Section titled “Requirements”- Servers MUST issue a fresh, unpredictable nonce per attempt
- Servers MUST expire a challenge quickly; 60 seconds is the default
- Servers MUST NOT reuse a nonce across attempts
- Servers MUST NOT reveal whether
prefixis already registered
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/mailboxes/challengeRegisterMailbox
Section titled “RegisterMailbox”Creates a mailbox registration, or extends an existing one.
- Input:
RegistrationRequest { prefix, proof } - Output:
Registration { prefix, expires_at } - Auth: PrivacyPass, Mailbox Ownership
- Idempotent: By
prefix- re-registering extends the registration - Errors:
TOKEN_INVALID,TOKEN_REPLAYED,BUDGET_EXHAUSTED,MAILBOX_EXISTS,BINDING_INVALID,CHALLENGE_EXPIRED,CHALLENGE_INVALID
Requirements
Section titled “Requirements”- Registration MUST spend one token
- Servers MUST verify that
prefixderives fromproof.mailbox_public - Servers MUST verify
proof.signatureagainst the challenge nonce - Servers MUST NOT accept a static signature in place of challenge-response
- Servers MUST reject a prefix already registered to a different
mailbox_public - Servers MUST treat a request under the same
mailbox_publicas a renewal and extendexpires_at - Registrations MUST expire; 24 hours is the default
- Servers MUST NOT record anything relating one registration to another
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/mailboxesRenewMailbox
Section titled “RenewMailbox”Renewal is essentially RegisterMailbox with the same prefix and a fresh proof. There is no separate operation: one registration path is easier to reason about than two, and the token cost is what bounds mailbox count.
A device in ten groups spends ten tokens a day keeping its mailboxes alive, against 12,000 issued daily at Established tier. The cost is not the point of the charge; uniformity is.
OpenRetrievalSession
Section titled “OpenRetrievalSession”Exchanges a proof for a short-lived session authorizing retrieval from one mailbox.
- Input:
SessionRequest { prefix, proof } - Output:
Session { token, expires_at } - Auth: Mailbox ownership
- Idempotent: No
- Errors:
MAILBOX_UNKNOWN,BINDING_INVALID,CHALLENGE_EXPIRED,CHALLENGE_INVALID
Requirements
Section titled “Requirements”- A session MUST be scoped to exactly one mailbox
- Servers MUST NOT accept one session as authorization for another mailbox
- Servers MUST NOT retain any record relating two sessions to each other
- Sessions MUST be short-lived; one hour is the default
- Clients MUST establish sessions independently per mailbox
- Retrieval spends no token
HTTP Binding
Section titled “HTTP Binding”POST /api/v1/mailboxes/{prefix}/sessionDeregisterMailbox
Section titled “DeregisterMailbox”Retires a mailbox before its registration expires.
- Input:
DeregisterRequest { prefix, proof } - Output: None
- Auth: Mailbox ownership
- Idempotent: Yes
- Errors:
MAILBOX_UNKNOWN,BINDING_INVALID,CHALLENGE_EXPIRED,CHALLENGE_INVALID
Requirements
Section titled “Requirements”- Servers MUST require a fresh proof; deregistration is destructive
- Servers MUST discard queued envelopes for the mailbox
- Deregistration spends no token - it frees resources rather than consuming them
- Servers MUST return success for a prefix that is already gone
HTTP Binding
Section titled “HTTP Binding”DELETE /api/v1/mailboxes/{prefix}Rotation
Section titled “Rotation”Rotation is not a server operation. A device derives a fresh keypair, registers the new mailbox, broadcasts an AddressRotation inside MLS, and lets the old mailbox lapse after MAILBOX_ROTATION_OVERLAP.
The server sees two unrelated registrations and one expiry. It cannot tell a rotation from an unrelated device arriving and another leaving, which is the point.
- The previous mailbox MUST stay registered for the overlap window, which means renewing it
- Devices MUST NOT renew the previous mailbox once the overlap closes
- A member who missed the
AddressRotationrecovers viaAddressBookSnapshot