Note to implementers: The moderncrypto.org thread on saltpack brought up an issue with our nonces, which weakens the anonymity of receivers. Fixing this requires backwards-incompatible changes, and will likely lead to a version 2 of this spec.
Building on what NaCl boxes give us, there are several other properties we want our messages to have:
At a high-level, the message is encrypted once using a symmetric key shared across all recipients. It is then MAC'ed for each recipient individually, to preserve the intent of the original sender, and to prevent recipients from maliciously altering the message. For example, if Alice is sending to Bob and Charlie, Bob should not be able to rewrite the message and pass it to Charlie without Charlie detecting the attack.
The message is chunked into 1MB chunks. A sequential nonce used for the encryption and MAC's ensures that the 1MB chunks cannot be reordered. The end of the message is marked with an empty chunk — encrypted and MAC'ed the same way — to prevent truncation attacks.
Though the scheme is designed with the intent of having multiple per-device keys for each recipient, the implementation treats all recipient keys equivalently. That is, sending a message to five recipients with four keys each is handled the same as sending to one recipient with twenty keys, or twenty recipients with one key each.
Finally, the scheme accommodates anonymous receivers and anonymous senders. Thus, each message needs an ephemeral sender public key, used only for this one message, to hide the sender's true identity. Some implementations of the scheme can choose to reveal the keys of the receivers to make user-friendlier error messages on decryption failures (e.g., "Can't decrypt this message on your phone, but try your laptop.") If the sender wants to decrypt the message at a later date, she simply adds her public keys to the list of recipients.
An encrypted message is a series of concatenated MessagePack objects. The first is a header packet, followed by any number of non-empty payload packets, and finally an empty payload packet.
The header packet is a MessagePack list with these contents:
[ format name, version, mode, ephemeral public key, sender secretbox, recipients list, ]
crypto_secretboxcontaining the sender's long-term public key, encrypted with the payload key from below.
A recipient pair is a two-element list:
[ recipient public key, payload key box, ]
crypto_boxcontaining a copy of the payload key, encrypted with the recipient's public key and the ephemeral private key.
When composing a message, the sender follows these steps to generate the header:
crypto_secretboxwith the payload key and the nonce
saltpack_sender_key_sbox, to create the sender secretbox.
crypto_boxwith the recipient's public key, the ephemeral private key, and the nonce
saltpack_payload_key_box. Pair these with the recipients' public keys, or
nullfor anonymous recipients, and collect the pairs into the recipients list.
crypto_hash(SHA512) of the bytes from #6. This is the header hash.
Serialize the bytes from #6 again into a MessagePack
bin object. These
twice-encoded bytes are the header packet.
After generating the header, the sender computes the MAC keys, which will be used below to authenticate the payload:
For each recipient, encrypt 32 zero bytes using
crypto_box with the recipient's public
key, the sender's long-term private key, and the first 24 bytes of the
header hash from #8 as a nonce. Take the last 32 bytes of each box.
These are the MAC keys.
Encrypting the sender's long-term public key in step #3 allows Alice to stay anonymous to Mallory. If Alice wants to be anonymous to Bob as well, she can reuse the ephemeral keypair as her own in steps #3 and #9. When the ephemeral key and the sender key are the same, clients may indicate that a message is "intentionally anonymous" as opposed to "from an unknown sender".
Using the same nonce for each payload key box might raise a concern: Are we violating the rule against nonce reuse, if for example the recipients list happens to contain the same recipient twice? No, because each of these boxes holds the same plaintext, reusing a key and a nonce will produce exactly the same ciphertext twice.
Recipients parse the header of a message using the following steps:
crypto_hash(SHA512) of the bytes from #1 to give the header hash.
crypto_box_beforenmwith the ephemeral public key and the recipient's private key.
crypto_box_open_afternm, the precomputed secret from #5, and the nonce
saltpack_payload_key_box. Successfully opening one gives the payload key, and the index of the box that worked is the recipient index.
crypto_secretbox_openwith the payload key from #6 and the nonce
crypto_boxwith the recipient's private key, the sender's public key from #7, and the first 24 bytes of the header hash from #2 as a nonce. The MAC key is the last 32 bytes of the resulting box.
If the recipient's public key is shown in the recipients list (that is, if the recipient is not anonymous), clients may skip all the other payload key boxes in step #6.
When parsing lists in general, if a list is longer than expected, clients should allow the extra fields and ignore them. That allows us to make future additions to the format without breaking backward compatibility.
Note that the only time the recipient's private key is used for decryption (with the payload key box), it's used with a hardcoded nonce. So even if Bob uses the same public key with multiple applications, Bob's saltpack client will never open a box that wasn't intended for saltpack.
A payload packet is a MessagePack list with these contents:
[ authenticators list, payload secretbox, ]
NNNNNNNNis the packet number as an 8-byte big-endian unsigned integer. The first payload packet is number 0.
Computing the MAC keys is the only step of encrypting a message that requires the sender's private key. Thus it's the authenticators list, generated from those keys, that proves the sender is authentic. We also include the hash of the entire header as an input to the authenticators, to prevent an attacker from modifying the format version or any other header fields.
We compute the authenticators in three steps:
crypto_hash(SHA512) of the bytes from #1.
crypto_auth(HMAC-SHA512, truncated to 32 bytes) of the hash from #2, using that recipient's MAC key.
The recipient index of each authenticator in the list corresponds to the
index of that recipient's payload key box in the header. Before opening the
payload secretbox in each payload packet, recipients must first verify the
authenticator by repeating steps #1 and #2 and then calling
Unlike the twice-encoded header above, payload packets are once-encoded directly to the output stream.
After encrypting the entire message, the sender adds an extra payload packet with an empty payload to signify the end. If a message doesn't end with an empty payload packet, the receiving client should report an error that the message has been truncated.
The authenticators cover the SHA512 of the payload, rather than the payload itself, to save time when a large message has many recipients. This assumes the second preimage resistance of SHA512, in addition to the assumptions that go into NaCl.
crypto_secretbox to encrypt the
payload takes more time and 16 bytes more space than
crypto_stream_xor would. Likewise, using
crypto_box to compute the MAC keys takes
more time than
Nonetheless, we prefer box and secretbox for ease of implementation. Many
languages have NaCl libraries that only expose these higher-level
# header packet (on the wire, this is twice-encoded) [ # format name "saltpack", # major and minor version [1, 0], # mode (0 = encryption) 0, # ephemeral public key 895e690ba0fd8d15f51adf59e161af3f67518fa6e2eaadd8a666b8a1629c2349, # sender secretbox b49c4c8791cd97f2c244c637df90e343eda4aaa56e37d975d2b7c81d36f44850d77706a51e2ccd57e7f7606565db4b1e, # recipient pairs [ # the first recipient pair [ # recipient public key (null in this case, for an anonymous recipient) null, # payload key box c16b6126d155d7a39db20825d6c43f856689d0f8665a8da803270e0106ed91a90ef599961492bd6e49c69b43adc22724, ], # subsequent recipient pairs... ], ] # payload packet [ # authenticators list [ # the first recipient's authenticator f90b04186d599b42779564fc93535e1de486de5fbb98e8a987487799910c10c8, # subsequent authenticators... ], # payload secretbox f991dbe030e2cfa00a640376f956c68b2d113ec6384441a1834e455acbb046ead9389826e92cb7f91cc7ab30c3d4d38ef5e84d12617f37, ] # empty payload packet [ # authenticators list [ # the first recipient's authenticator 0bd743eb8b9c48ba1888d265cd90dedcc3d56b0a003ef99763af224c1a6501db, # subsequent authenticators... ], # the empty payload secretbox 481ef99b4f0d0f918edd82e9f5619a41, ]