Encryption
The full cryptographic design: how a mailbox key is derived from one signature, how a message is sealed, what the threat model covers, and where the honest limits are.
bMail uses NaCl crypto_box: X25519 for key agreement and
XSalsa20-Poly1305 for authenticated encryption. These are conservative, widely
reviewed primitives. Nothing here is novel cryptography, which is the point.
Key derivation
Sealing a message
Each message uses a fresh ephemeral keypair. The ephemeral secret is used for
two crypto_box operations and then discarded.
// 1. build a payload that commits to its own routing
payload = { subject, body, sentAt, from, to, chainId, mailbox, skv }
// 2. pad so length leaks less
msg = pad(utf8(json(payload))) // up to a multiple of 256 bytes
// 3. one ephemeral keypair, two recipients
eph = box.keyPair()
r = box(msg, nonce_r, recipientPub, eph.secret) // for them
s = box(msg, nonce_s, senderPub, eph.secret) // for your own sent copy
Why the payload commits to its routing
Without from, to, chainId and
mailbox sealed inside, an envelope is just a blob that decrypts.
Anyone could copy it out of the log and re-emit it as their own message to a
different recipient, or replay it on a second deployment, and it would open
cleanly and look authentic. Binding the routing into the authenticated plaintext
makes such a copy detectable: the client compares what it decrypted against the
event topics it actually observed and rejects a mismatch.
Why two ciphertexts
crypto_box seals to one recipient. Sealing only to the recipient
would leave the sender unable to read their own outbox. The second ciphertext is
sealed to the sender's own key from the same ephemeral secret, and the client
opens whichever one matches its role.
Why padding
Ciphertext length is public. A one-line "yes" and a long contract are distinguishable by size alone. Padding to 256-byte blocks collapses messages into size buckets. It reduces the leak; it does not eliminate it.
Peer key screening
Both the recipient's published key and the ephemeral key on an incoming envelope are screened before use. The check has three parts:
- Reject the six known small-order Curve25519 points.
- Reject non-canonical encodings, including a set top bit.
- Probe the resulting shared secret and reject it if it is all zeroes.
The first audit found that an attacker could publish a small-order key and make every message sealed to it world-readable. The fix was applied to the sending side. The second audit found the incoming ephemeral key was still unchecked, which reopened the same hole from the other direction. Both directions are screened now.
Key rotation
rotateKey(newPubkey) publishes a new key and increments
keyVersion. Old versions stay in keyHistory, so mail
sealed to a previous key remains readable. Envelopes record which version they
were sealed to in their kv field, and the client re-derives the
matching historical key on demand.
Rotation therefore limits forward exposure without destroying your archive. It does not retroactively protect messages an attacker already captured and can decrypt with the compromised key.
Threat model
| Adversary | Outcome |
|---|---|
| Observer with the full chain history | Sees who mailed whom, when, and roughly how large. Cannot read any content. |
| Malicious RPC provider | Can withhold or delay data and lie about state. Cannot decrypt, and cannot forge a valid envelope. |
| Attacker who publishes a hostile key | Rejected by screening on both the sending and receiving sides. |
| Attacker replaying a copied envelope | Rejected: the sealed routing will not match the observed event. |
| Attacker who steals your wallet | Full compromise. They can re-derive the mailbox key and read everything. |
| Attacker who obtains one derivation signature | Reads all mail at that key version. Rotation limits future exposure only. |
| Compromised or spoofed web client | Full compromise. A malicious page can capture the signature at derivation time. |
Honest limits
No forward secrecy across a key version
Ephemeral keys are per message, but everything at one key version derives from one long-lived secret. Compromise that secret and the whole version is readable.
Authorship is chain-bound, not proven
A valid tag proves integrity, not who wrote it. A per-account signing key would make authorship provable independently. It is on the roadmap.
Metadata is public
The social graph is visible to anyone. Encryption does not address this; stealth addressing would.
Lose the wallet, lose the mailbox
There is no recovery path, because a recovery path is by definition someone else who can read your mail.
Planned: envelope version 3
The format carries a version byte specifically so the primitive can change without breaking old mail. Version 3 migrates to XChaCha20-Poly1305. Clients will continue to open version 2 envelopes indefinitely.