Technical

Message format & cryptography

Each message has a fresh random 256-bit content key used with ChaCha20-Poly1305. How that key reaches the recipient depends on their key type:

Recipient keyHow the content key is sharedSender signature
ssh-ed25519X25519 ECDH with an ephemeral key (Edwards → Montgomery mapping, as age does), then HKDF-SHA256Ed25519
ecdsa-sha2-nistp256/384/521ECDH on the key’s own curve with an ephemeral key; the peer’s point is validated; then HKDF-SHA256ECDSA (SHA-256/384/512, RFC 6979 nonces)
ssh-rsa ≥ 2048 bitsRSA-OAEP (SHA-256, MGF1-SHA-256) wraps the content keyrsa-sha2-512

The envelope

envelope := "WHSP" ‖ 0x01 ‖ string(kem) ‖ string(kem_data) ‖ nonce[12] ‖ ciphertext ‖ tag[16]
kem      := "x25519" | "ecdh-nistp256" | "ecdh-nistp384" | "ecdh-nistp521" | "rsa-oaep-sha256"
payload  := string(sender public key, SSH wire format) ‖ uint64(time_ms)
            ‖ string(SSH signature blob) ‖ string(utf8 text) ‖ [string(utf8 sender name)]
            ‖ zero padding to a multiple of 64 bytes

string(x) is the SSH wire encoding: a big-endian uint32 length followed by the bytes.

  • Authenticated as a whole. Everything up to and including the nonce is the AEAD’s associated data, so the header can’t be altered.
  • Signed inside. The sender’s SSH signature covers the recipient’s key, the key-exchange data, the timestamp and the text. That binds the message to one recipient and one ciphertext: it can’t be decrypted and re-encrypted to someone else as if the sender had written it to them.
  • Recipient not named. Nothing in the envelope says who it’s for; only the right private key can open it.
  • Padded. The payload is padded to a multiple of 64 bytes to hide the exact length of the text.

The sender name

The optional sender name (at most 80 characters) lets someone who hasn’t added you yet reply without an invite back. Older readers stop after the text, and when there’s no name the zero padding reads as an empty one. Like the name on an invite, it is only what the sender says: the encryption protects it on the way, but the signature does not cover it. The ✓ in the UI comes from matching the signing key against the address book, never from the name.

Encodings

The same envelope is shown either as a link (base64url in the fragment) or as an armored block for mail apps that mangle links:

-----BEGIN WHISPER MESSAGE-----
V0hTUAEAAAAGeDI1NTE5AAAAIEYWU0ucvl2lPUeB5YcqkA2nt2F2FP/6HsBU/D5U
…
-----END WHISPER MESSAGE-----

Emails carry both; texts carry just the link.

Sizes

Measured when sender and recipient use the same key type:

KeysEmpty messageLink, emptyLink, 100-character message
ed25519271 bytes374 chars459 chars
ECDSA P-256375 bytes512 chars683 chars
ECDSA P-521571 bytes774 chars944 chars
RSA 2048888 bytes1,196 chars1,367 chars

A sender name adds its length plus 4 bytes before padding, so usually at most one more 64-byte block. There is no hard limit, but SMS and some chat apps truncate long links — which is why ed25519 is the default.