The previous version of this spec is available for applications that need backwards compatibility.
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 authenticated flag 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 one or more payload packets, the last of which is indicated with a final packet flag.
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, the ephemeral private key, and a counter nonce.
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
XXXXXXXXis 8-byte big-endian unsigned recipient index, where the first recipient is index zero. 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 each recipient's MAC key, which will be used below to authenticate the payload:
Concatenate the first 16 bytes of the header hash from step 7 above, with the recipient index from step 4 above. This is the basis of each recipient's MAC nonce.
nonce &= 0xfe.
crypto_boxwith the recipient's public key, the sender's long-term private key, and the nonce from the previous step.
nonce |= 0x01.
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".
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
XXXXXXXXis 8-byte big-endian unsigned recipient index, where the first recipient is index 0. Successfully opening one gives the payload key.
crypto_secretbox_openwith the payload key from #6 and the nonce
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:
[ final flag, 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.
If a message ends with without setting the final flag to true, the receiving client must 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 [2, 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, # final flag True, ]