This document specifies VERSION 1 of the saltpack encryption format. This is a legacy reference, for clients that issued version 1 messages in the past and need to maintain read support. All new saltpack encryption messages should use VERSION 2.
Note to implementers: The moderncrypto.org thread on saltpack brought up an issue with our nonces, which weakens the anonymity of receivers. This issue is fixed in version 2.
The main building block of our encrypted message format is NaCl's box and secretbox constructions. These have several properties that we'll want to keep:
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 1MiB (= 2^20 bytes) chunks. A sequential nonce used for the encryption and MAC's ensures that the 1MiB 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.
When encoding strings, byte arrays, or arrays, pick the MessagePack encoding that will use the fewest number of bytes.
The header packet is a MessagePack array with these contents:
[
format name,
version,
mode,
ephemeral public key,
sender secretbox,
recipients list,
]
[1, 0]
, both encoded as
positive fixnums.crypto_secretbox
containing 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_box
containing 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_box_keypair
.crypto_secretbox
with the payload
key and the nonce saltpack_sender_key_sbox
, to create the sender
secretbox.crypto_box
with the recipient's public
key, the ephemeral private key, and the nonce
saltpack_payload_key_box
. Pair these with the recipients' public keys,
or null
for anonymous recipients, and collect the pairs into the
recipients list.array
object.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_beforenm
with 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_open
with the
payload key from #6 and the nonce saltpack_sender_key_sbox
crypto_box
with 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 array with these contents:
[
authenticators list,
payload secretbox,
]
saltpack_ploadsbNNNNNNNN
where NNNNNNNN
is 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
crypto_auth_verify
.
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.
Using 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 crypto_stream
would.
Nonetheless, we prefer box and secretbox for ease of implementation. Many
languages have NaCl libraries that only expose these higher-level
constructions.
# 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,
]