Docs
/
Protocol

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

EIP-712 typed signature 65 bytes HKDF-SHA256 RFC 5869 version label as info X25519 secret clamped scalar tab memory only Public key published to MailRegistry screened before accepting DETERMINISTIC the same wallet reproduces the same key on any device, with nothing stored or synced The signature never becomes a transaction. chainId is deliberately absent from the domain, so a single mailbox identity spans BSC and opBNB. A fixed salt supplies the domain separation instead.
Figure 5. The private key exists only in the tab that derived it. Closing the tab discards it; reopening re-derives it.

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:

  1. Reject the six known small-order Curve25519 points.
  2. Reject non-canonical encodings, including a set top bit.
  3. Probe the resulting shared secret and reject it if it is all zeroes.
This was a real finding, twice

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

AdversaryOutcome
Observer with the full chain historySees who mailed whom, when, and roughly how large. Cannot read any content.
Malicious RPC providerCan withhold or delay data and lie about state. Cannot decrypt, and cannot forge a valid envelope.
Attacker who publishes a hostile keyRejected by screening on both the sending and receiving sides.
Attacker replaying a copied envelopeRejected: the sealed routing will not match the observed event.
Attacker who steals your walletFull compromise. They can re-derive the mailbox key and read everything.
Attacker who obtains one derivation signatureReads all mail at that key version. Rotation limits future exposure only.
Compromised or spoofed web clientFull 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.

bMail is an independent, community-built project. It is not affiliated with, endorsed by, or operated by Binance or BNB Chain.