npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

envelope-spec

v1.1.1

Published

specification for envelope encryption

Downloads

214

Readme

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              ║ |
 | ║                                   ║ |
 | ║                                   ║ |
 | ║                                   ║ |
 | ║                                   ║ |
 | ║                         ╔═════════╝ |
 | ╚═════════════════════════╝           |
 +---------------------------------------+

header_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

  • Go: https://github.com/cryptoscope/ssb/tree/private-groups/private/envelope
  • Node.js: https://github.com/ssbc/envelope-js