The previous version of this spec is available for applications that need backwards compatibility.
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 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.
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,
]
[2, 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, the ephemeral private key, and a counter nonce.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_recipsbXXXXXXXX
.
XXXXXXXX
is 8-byte big-endian unsigned recipient index, where the first
recipient is index zero. 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 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[15] &= 0xfe
.crypto_box
with the recipient's public key, the sender's long-term private key, and
the nonce from the previous step.nonce[15] |= 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_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_recipsbXXXXXXXX
. XXXXXXXX
is
8-byte big-endian unsigned recipient index, where the first recipient is
index 0. Successfully opening one gives the payload key.crypto_secretbox_open
with the
payload key from #6 and the nonce saltpack_sender_key_sbox
.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:
[
final flag,
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.
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.
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
[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
[
# final flag
True,
# authenticators list
[
# the first recipient's authenticator
f90b04186d599b42779564fc93535e1de486de5fbb98e8a987487799910c10c8,
# subsequent authenticators...
],
# payload secretbox
f991dbe030e2cfa00a640376f956c68b2d113ec6384441a1834e455acbb046ead9389826e92cb7f91cc7ab30c3d4d38ef5e84d12617f37,
]