envelope spec | version 1.0.0
This is a spec for encrypting messages to groups of people.
Initially it will support communication for large groups which share a public key
(secret key cryptography / symmetric keys), but it has also been designed to support
forward-secure secret-key cryptography (a little like Signal's double-ratchet).
envelope assumes each message is part of append-only chain (with a unique feed_id),
made up of backlinked messages such that each message has a unique previous message with
a unique id (prev_msg_id)
Anatomy
After boxing, a complete envelope message looks like this:
+---------------------------------------+
| βββββββββββββββββββββββββββββββββββββ |
| β header_box β |
| βββββββββββββββββββββββββββββββββββββ |
| βββββββββββββββββββββββββββββββββββββ |
| β key_slot_1 β |
| βββββββββββββββββββββββββββββββββββββ€ |
| β key_slot_2 β |
| βββββββββββββββββββββββββββββββββββββ€ |
| β ... β |
| βββββββββββββββββββββββββββββββββββββ€ |
| β key_slot_n β |
| βββββββββββββββββββββββββββββββββββββ |
| βββββββββββββββββββββββββββββββββββββ |
| β extensions β |
| β β |
| βββββββββββββββββββββββββββββββββββββ |
| βββββββββββββββββββββββββββββββββββββ |
| β body_box β |
| β β |
| β β |
| β β |
| β β |
| β βββββββββββ |
| βββββββββββββββββββββββββββ |
+---------------------------------------+
A secretbox (refering to libsodium crypto_secretbox_easy), which describes the layout
and configuration of the message to follow.
Being able to decrypt this is required for being able to unbox the rest of the message.
βββββββββββββββββββββββββββββββββββ
β header_box β
βββββββββββββββββββββββββββββββββββ
| 32 |
| |
βββββββββββββββββββ¬ββββββββββββββββ
β HMAC β header* β
βββββββββββββββββββ΄ββββββββββββββββ
16 / 16 \
/ \
/ \
/ \
/ \
/ \
+----------------+-------+-------------------- ---+
| offset | flags | header_extensions |
+----------------+-------+-------------------- ---+
2 1 13
HMAC - 16 bytes which allows authentication of the integrity of header*
header* - the header encrypted with header_key + zerod nonce
offset - 2 bytes which desribe the offset of the start of body_box in bytes
flags - 1 byte where each bit describes which extensions are active (if any)
header_extensions - 13 bytes for configuration of extensions
key_slot_n
Each of these slots is like a 'safety deposit box' which contains a copy of the top-level
msg_key which allows decryption of everything in the message.
The slots contents are defined by
slot_content = xor(
msg_key,
Derive(recipient_key, ["slot_key", key_mgmt_scheme], 32)
)
Where
Derive is the same derivation function defined here
recipient_key is one of the shared keys you're encrypting to
could be:
- a private key for a group (symmetric key)
- a double-ratchet derived key for an individual (this option requires more info in the
header_extensions + extensions)
key_mgmt_scheme is the type of recipient_key, specifically what sort of key management it's involved in, e.g. :
- "envelope-large-symmetric-group"
- "envelope-id-based-dm-converted-ed25519"
- "envelope-signed-dh-key-curve25519"
Note these slots have no HMAC. This is because if you successfully extract msg_key from one of
these slots you can immediately confirm if you can decrypt the header_box, which has an HMAC,
which will confirm whether you have the correct key
extensions
...WIP
This is where things like keys for double-ratchet-like communication will go.
This section might also contain padding.
body_box
The section which contains the plaintext which we've boxed.
βββββββββββββββββββββββββββββββββββ
β body_box β
β β
β β
β β
β β
β βββββββββ
βββββββββββββββββββββββββββ
| >=16 |
| |
βββββββββββββββββββ¬ββββββββββββββββ
β HMAC β β
βββββββββββββββββββ β
β β
β body* β
β β
β βββββββββ
βββββββββββββββββββββββββββ
HMAC - 16 bytes which allows authentication of the integrity of body*
body* - the body encrypted with body_key and a zerod nonce
Unboxing algorithm
When you receive a envelope message, the only things you know are:
- the length of the whole box (doesn't tell you much, as there may be padding)
- where the key-slots start (because the header_box is exactly 32 bytes)
- where this message was posted (we call this it's "context", and the boxing is bound to this)
- which
feed_id posted it
- what the
prev_msg_id was (i.e. what was the message before it in this feed_ids chain?)
So starting after the header_box (32 bytes in), we lift out successive chunks of 32 bytes
(the size of a key_slot) chunks and try and decrypt them.
The way we know if a key_slot has yielded us a valid key for the message is by trying to see
if the "key" we've derived from a slot helps us decrypt the header_box. This works because
the header_box has an HMAC, which is an authentication code which allows us know know if our
decryption is valid.
If the first slot doesn't yield a valid key, we move to the second slot (starting 32 + 32 bytes
into the box), and check the next slot. We either try incrementing through the whole box till
we succeed (or reach the end), OR we set a "max depth" we want to try (e.g. if we think there
will not be more than 10 slots, we can quit after (32 + 10 * 32 bytes).
Once we have the msg_key, we can decrypt the header_box. This reveals offset - the position
of the start of the body_box in bytes. This allows us to proceed to decrypt the body of the original message.
Futher detail:
Design
Original notes from a week long design session Dominic + Keks did.
(scuttlebutt: %39f9I0e4bEln+yy6850joHRTqmEQfUyxssv54UANNuk=.sha256)
Key derivation
Keys are derived from msg_key as follows
msg_key
β
βββ> read_key = Derive(msg_key, "read_key", 32)
β β
β βββ> header_key = Derive(read_key, "header_key", 32)
β β
β βββ> body_key = Derive(read_key, "body_key", 32)
β
βββ> extensions = Derive(msg_key, "extentions", 32)
β
βββ> TODO
Where the Derive function is defined here
msg_key is the symmetric key that is encrypted to each recipient or group.
When entrusting the message, instead of sharing the msg_key instead the message read_key is shared.
this gives access to header metadata and body but not ephemeral keys.
Implementations