How Encryption Works
EtherPK encrypts a synced graph on your device before any of it is sent, so the Sync Server stores and relays data it cannot read. The sections below describe the keys, the algorithms, the data formats and the limits of the design, with enough detail to check each claim against the code. Synced Graphs, Recovery Code And Device Approval, Device Passcode and Protected Documents describe the same features in everyday terms.
All of the encryption runs in the EtherPK Client, and the Headless Client uses the same code. The Client is source-available on GitHub as etherpk-client, so you can read every line that handles a key (Source code).
Goals and trust model
The design treats the Sync Server as a store and a relay that must never be able to read your notes. The server can be run by someone you do not trust, or be taken over by an attacker, and it still holds only ciphertext and public keys.
- The Client encrypts and authenticates content on your device, with keys the server never receives.
- The Client signs what it sends about keys: every write of your Encryption Keys, every invite and every copy of a graph's key that it hands to another member. The server refuses a key write that your key did not sign, so someone with only your sign-in cannot change your Encryption Keys. The Client that receives an invite or a copy of a graph's key checks its signature too, so the server cannot forge either. Comparing security fingerprints with someone confirms that the keys you were given for them are theirs (Sharing a graph).
- Signing in and encryption are separate. Your sign-in credential, an EtherPK Account session, a Personal Access Token or the Headless Client's agent token, proves who you are to the server and cannot decrypt anything. The Client derives no encryption key from your account password.
- Each Sync Server is separate. Your account on one server has its own Encryption Keys and its own Recovery Code, and nothing is shared with your account on another server.
Connections to the managed service use TLS (HTTPS and secure WebSockets). The encryption on this page does not depend on TLS, but TLS protects your sign-in credential and the metadata in transit.
Staying safe says where to take care.
The keys
An account here means your account on one Sync Server. The vault is one encrypted record per account. It holds your identity private keys, the Graph Key of every synced graph you belong to on that server, the protection records of your synced graphs, and the keys of other people EtherPK has pinned (Sharing a graph). The server stores the vault and cannot open it. Your identity keys and the Graph Keys you hold are together called your Encryption Keys.
Keys that encrypt content
- Graph Key. 256 random bits for each graph, numbered by epoch. It encrypts the graph's documents, cursors, name and file metadata. Every member keeps a copy in their vault, and a new epoch starts whenever somebody leaves the graph (Membership changes).
- File key. 256 random bits for each file. It encrypts the contents of that one file, and it is stored in the file's metadata under the Graph Key.
- Protection Key. 256 random bits for each graph and each person. It encrypts your protected documents, and it is stored wrapped under your passphrase, and optionally under a passkey.
Keys that protect other keys
- Vault key. 256 random bits for each account. It encrypts your vault. The vault record holds it wrapped, and each unlocked device keeps a copy.
- Recovery Code. 128 random bits for each account, shown to you once. The Client derives the key that wraps the vault key from it.
- Identity keys. Two key pairs for each account. An owner who invites you, or hands you a new Graph Key, sends it to your X25519 key pair, which encrypts nothing else. Your Ed25519 key pair signs your key writes, your invites and the copies of Graph Keys you hand out. The private keys are in your vault, and the public keys are on the server.
- Device passcode key. Optional, for each device. When you set a passcode, the Client stretches it into a key that encrypts the copies of your vault keys and access tokens kept on that device (Encryption Keys and data on your devices).
A Graph Key is a list of keys rather than a single key. Each entry has an epoch number, and every record encrypted under a Graph Key names the epoch that encrypted it, so a graph can move to a new key and still read its history.
The Client uses AES-256-GCM for all encryption with a shared key. AES-256-GCM is an authenticated cipher, so decryption fails if anyone changes the ciphertext or the label that goes with it. It uses Ed25519 for every signature.
Creating your Encryption Keys
The Client creates your Encryption Keys the first time you need them, with your first synced graph or with Create Encryption Keys on the Sync tab:
- The Client generates your X25519 and Ed25519 identity key pairs, each from 32 random bytes.
- It generates your Recovery Code from 16 random bytes.
- It derives the vault wrap key from the Recovery Code with HKDF-SHA-256. HKDF is a standard function that turns a secret into keys.
- It generates a random 256-bit vault key and encrypts the vault under it.
- It encrypts the vault key under the wrap key and stores both results in one record.
- It shows you the Recovery Code. The Client uploads the vault and your identity public keys only after you tick the box that says you saved the code. If you close the dialog first, nothing reaches the server.
The first upload is signed with the new Ed25519 key, which shows the server that the device holds the key it registers. From then on the server accepts a write of your vault or your identity keys only when the key it has on record signed it.
Accounts whose Encryption Keys were created by an earlier version of EtherPK have no Ed25519 key yet. The Client adds one the next time it unlocks the account's Encryption Keys, and that write must also prove that the device holds the account's X25519 private key. The server offers a key of its own for the proof, and the Client sends an HMAC of the write under a key that only the server and the holder of the X25519 private key can derive (Key derivations). Someone with your sign-in but not your Encryption Keys cannot add an Ed25519 key to the Encryption Keys you have, so they cannot sign anything with them. Reset Encryption Keys needs no key, because it is the way back for someone who has lost their Encryption Keys. It deletes the graphs you own, so the server refuses it to every access token. On a custom Sync Server it needs a sign-in to the server's own pages from the last 10 minutes, and on Managed Sync it needs the Client's managed sign-in. Keep your sign-in safe (Staying safe).
The device then keeps the vault key, so it stays unlocked (Encryption Keys and data on your devices).
When you create a synced graph, the server gives it a random id and records you as its owner. The Client generates the graph's first Graph Key, epoch 1, from 32 random bytes, adds it to your vault, and uploads the vault again.
Encrypting documents
Each document in a synced graph is a Yjs document. Yjs is a library that merges edits made on several devices at the same time, and it records every change as a binary update. The Client encrypts each update before it leaves the device:
- It encrypts the update with AES-256-GCM under the newest epoch of the Graph Key, with a new random 96-bit nonce. A nonce is a value that must not repeat under one key.
- The encryption also covers a label, the associated data, which names the purpose, the graph and the document. The label is not secret, but decryption fails if it is different, so the server cannot move a ciphertext to another document or graph.
- The Client puts the encrypted update in its outbox in browser storage, and then sends it. The server adds it to the log of the document.
Once a graph has moved to a new epoch, the server refuses a write encrypted under an older one. The Client then collects its copy of the new key and encrypts the write again. The server reads the epoch number from the header of each encrypted update, which is not encrypted (Envelope formats). It also refuses a write whose stated epoch differs from that number, and a write under an epoch the graph has not reached, which no member could decrypt.
From time to time a device that has the document open uploads a snapshot, which is the full state of the document, encrypted in the same way. The server deletes the older updates only after the device that made the snapshot has read it back, decrypted it and checked that it matches the state it captured. The clients do this work because the server cannot read what it stores.
The Graph Key also protects the following:
- The graph's root document, which holds the graph name, the shared settings and the list of documents with their names. It is encrypted like any other document, so the server cannot see page names.
- The cursors and selections of people who edit together, which have a label of their own. The server relays them and does not store them.
- A second copy of the graph name, which the graph list uses before the graph is open. The Client pads the name to a multiple of 64 bytes before it encrypts it, so the length of the ciphertext shows little about the name.
Encrypting files
The Client encrypts images and other files in chunks, under a key for each file:
- The Client generates a random 256-bit file key and a random file id.
- It splits the file into chunks of 4 MiB and encrypts each chunk with AES-256-GCM under the file key. The label of each chunk names the file id and the chunk number, so chunks cannot be swapped or put in a different order.
- It encrypts the file's metadata under the Graph Key. The metadata holds the file name, the file type, the SHA-256 hash of the contents, the size, the number of chunks and the file key.
- It uploads each chunk directly to object storage, through a signed address that the Sync Server issues. Objects are named by the graph id, the file id and the chunk number, never by the file name.
A download works the other way. The server checks that you are a member of the graph and returns the encrypted metadata, with a signed address for each chunk that expires after 15 minutes. The Client checks what arrives against the metadata:
- It refuses before downloading anything if the server offers a different number of chunks.
- After joining the chunks, it checks the size and the SHA-256 hash.
- If a check fails, it fetches the file once more. If that fails too, it opens nothing and keeps nothing, and says the file arrived incomplete.
Files uploaded by an earlier version of EtherPK have no size or chunk count in their metadata, so the Client checks them by hash only.
The Client also stops the same file being stored twice in one graph. It sends the server a token with each upload, which is an HMAC-SHA-256 of the file's hash under a secret derived from the graph's newest Graph Key epoch. HMAC is a keyed hash, so the server cannot calculate a token or get the hash back from it. When the server answers that the file is already stored, the Client opens that file's metadata and reuses it only if the hash matches its own. Otherwise it uploads a fresh copy. Each graph has its own secret, so tokens from two graphs cannot be compared.
Sharing a graph
An invite carries the Graph Key to the new member in a sealed box, and the owner signs it. A sealed box is encrypted to a public key, so only the holder of the matching private key can open it.
- The owner's Client asks the server for the invitee's identity public keys.
- It shows the security fingerprint of those keys, which is the first 128 bits of a SHA-256 hash over both of them. The owner compares it with the fingerprint that the invitee sees on their own device, by a route the server is not on, such as a call.
- The Client seals the Graph Key, with every epoch, and the graph name to the invitee's X25519 key. The label names the graph id.
- It signs the invite with the owner's Ed25519 key. The signature covers the graph, both accounts, the invitee's keys and a hash of the sealed box.
- The server checks the signature against the owner's key and that the box was sealed to the invitee's current key, and stores the invite.
- The invitee's Client checks the signature against the owner's published key and shows the owner's fingerprint to compare. Once the invitee confirms it, the Client opens the box with their X25519 private key, adds the Graph Key to their vault and uploads the vault.
The fingerprint check is what stops a server from handing over keys of its own, which would let it open the sealed box or sign a fake invite. When you confirm a fingerprint, your Client remembers that person's keys as a pin in the vault and shows them as Verified. If the server later presents different keys for a pinned person, the Client shows Key changed and uses nothing sealed or signed with the new keys until you compare fingerprints again. In the member list of a graph you own, a member whose fingerprint you have not compared shows as Unverified. The Client refuses an invite with no signature, or one whose signature does not check out.
The Client never replaces a Graph Key it already holds. An invite for a graph whose key is already in your vault, such as an invite back after you left, adds only epochs you do not have. If an epoch you hold arrives under a different key, the Client refuses the invite.
Adding a device
A new device needs your vault key. It gets the key from another device that is already unlocked, or from your Recovery Code.
Approval from another device
Both devices contribute a one-time X25519 key pair, and neither can choose its key after seeing the other's:
- The new device generates its one-time key pair and sends the server only a commitment, a SHA-256 hash of its public key.
- An unlocked device generates its own one-time key pair and sends its public key.
- The new device then sends its public key. The server and the unlocked device check it against the commitment.
- Both devices hash the request, the commitment and both public keys into a transcript, and show
an 8-character code from it, for example
7Q4M-KX2A. - You compare the two codes. If they match, select Approve on the unlocked device and The codes match on the new one, in either order.
- The unlocked device encrypts the vault key under a key that only the two devices can derive, from their two one-time keys and the transcript. The new device checks that the key opens your vault, and uses it only once you have confirmed the codes match on it too.
Because the new device commits to its key first, a server that wants to substitute keys of its own cannot search in advance for a pair whose codes would match. A substitution gives matching codes only by chance, 1 in 2^40 (about one in a trillion) for each attempt, and every other attempt shows you two different codes. The server deletes the encrypted key when the new device collects it, and a request expires after 10 minutes.
Your Recovery Code
- The Client normalises what you typed. It changes letters to upper case, ignores spaces and
hyphens, and reads
Oas0andIorLas1. - It derives the vault wrap key from the code with HKDF-SHA-256.
- It gets your vault from the server, unwraps the vault key and opens the vault. A wrong or retired code fails at this step, and the Client stores nothing.
- It keeps the vault key on the device. It does not keep the code or the wrap key.
The Recovery Code needs no slow key derivation such as Argon2id, because it is 128 random bits. The server holds your vault and could try codes against it offline, but there are 2^128 possible codes, which is far too many to search.
Regenerating the Recovery Code
Regenerate Recovery Code makes a new code, derives a new wrap key from it, and wraps the same vault key again. The vault key does not change, so your other devices stay unlocked. The Client writes the new wrap only after you tick the box that says you saved the new code, and from then on the old code does not open the vault on the server.
Replacing your Encryption Keys
Replace Encryption Keys and lock out other devices is for a Recovery Code or Encryption Keys that someone may have copied. The Client makes a new Recovery Code, a new vault key and new identity key pairs, and encrypts the vault again under them. The change is signed twice, by your old Ed25519 key and by the new one. In one step the server then stores the new vault and keys, ends the sign-ins of your other devices on that server, revokes every access token issued before it, and cancels the pending invites to and from you, which were made with the old Encryption Keys. Every graph you own then moves to a new Graph Key epoch, and so does each graph whose invite to you was cancelled. A device still signed in to your EtherPK Account can sign in to the server again. It cannot unlock or change your new Encryption Keys, but it could still reset them, so sign it out under Sessions on your Account page.
Encryption Keys and data on your devices
EtherPK keeps these on your device, where your device and browser protect them:
- The vault key, in the browser's local storage for the Client's site, with one entry for each server and account, unless the device has a passcode.
- The access token for a custom Sync Server, in the same storage, unless the device has a passcode.
- The working copy of your synced documents in browser storage (IndexedDB), and the search index (in the Origin Private File System).
- On a computer that runs the Headless Client, its agent token and your Encryption Keys, in
the system keychain where it can use one (the macOS Keychain, or a Linux desktop's keyring) or
else in
~/.config/etherpk/mcp.json, which only your user can read, and its cache under~/.cache/etherpk/mcp/(The Headless Client). - Local graphs, Local Mirrors and exports, except for protected documents.
The following are encrypted on the device:
- The vault keys and access tokens, on a device with a passcode. The Client stretches the passcode with Argon2id into a key and encrypts each one under it. It asks for the passcode the first time it needs a key in a browser session, and an open EtherPK tab hands the key to a new one, so only the first tab asks. The tabs pass it through the browser's BroadcastChannel, which only pages of the Client's own site can use.
- Edits that are not yet sent. The outbox holds them encrypted under the Graph Key.
- A passkey wrap of a Protection Key, which stays in browser storage on that one device.
- Protected documents. The device stores the body of a protected document only in encrypted form, and the search index leaves it out. The Protection Key itself is never stored, and exists in memory only while the graph is unlocked.
Keep your browser profile to yourself, and install only browser extensions you trust, since they can see what your browser shows. A device passcode keeps the Encryption Keys EtherPK stores encrypted. The vault key stays on the device until you sign out, forget the server, use Remove for that server under Synced graphs in this browser on the This Device tab, or clear the site's data.
A device passcode can be as short as 4 characters, but choose one of 8 or more. A short passcode stops someone at the keyboard, but not someone who copies the device's storage.
Protected documents
Protected Documents add a second layer for documents that must stay unreadable on an unlocked device and to the other members of a shared graph.
- The Protection Key. It is 32 random bytes per graph, for each person, so two members of a shared graph have different keys. The Client never stores it unwrapped.
- The passphrase wrap. The Client stretches your passphrase with Argon2id and wraps the Protection Key under the result with AES-256-GCM. Argon2id is a password hashing function that needs a lot of memory, which makes each guess slow. Each record has its own random 16-byte salt and keeps its own Argon2id parameters.
- The passkey wrap. If you bind a passkey, the Client gets a secret from the authenticator through the WebAuthn PRF extension, with a fixed input for each graph. It derives a wrap key from the secret with HKDF-SHA-256 and wraps the Protection Key again. The wrap stays in that browser's storage and is never synced or exported, so a provider that syncs the passkey holds nothing that the secret can unwrap.
- Where the passphrase record is kept. On a synced graph it is in your vault, which other
members never see. On a local graph it is the file
etherpk/protection.jsonin the graph folder. - The envelope. The body of a protected document is one envelope, written as base64url text in a fenced code block. The header holds the time of the write and an 8-byte fingerprint of the key, and the encryption covers both. A member without the key can edit the document's text, but cannot make an old envelope look newer.
On a synced graph the envelope is part of the document, so it is encrypted again under the Graph Key. The frontmatter above the envelope, which holds the title, is not covered by protection.
Membership changes
An owner can remove a member, and any member can leave. When somebody leaves, for any reason, the server deletes their membership, refuses their requests from then on, closes their open connections, and marks the graph as due a new Graph Key epoch. The next time the owner's Client opens the graph or the graph list, it starts the new epoch:
- The server reserves the next epoch number and lists the remaining members with their current keys.
- The owner's Client generates a new 256-bit key and adds it to the graph's keys as the new epoch.
- For each member it checks their keys against its pin, and pins a member it has not pinned yet, as Unverified. Its notice names them, so the owner can compare fingerprints later. It seals the graph's keys, every epoch, to the member's X25519 key, and signs each copy with the owner's Ed25519 key. If a member's keys changed since they were pinned, it stops and asks the owner to compare fingerprints. If the server presents a pinned member without an Ed25519 key, which a pin always records, it stops too. There is nothing to compare then, so the owner asks whoever runs the server, or removes the member.
- The server checks that there is one copy for each current member, sealed to their current key and signed by the owner, and only then moves the graph to the new epoch.
- Each member's Client collects its copy, checks the signature against the owner's key and its pin of the owner, pinning the owner as Unverified if it has none, and adds the new epoch to its vault.
From then on everything is encrypted under the new epoch, and the server refuses writes under an older one. The former member still holds the epochs from before they left, so they can read anything encrypted under those epochs if they get the ciphertext some other way, for example from someone who runs the server. That includes what the other members write after the departure and before the owner's Client starts the new epoch. They cannot read anything written under a later epoch.
A member whose Encryption Keys were created by an earlier version of EtherPK, and who has not opened EtherPK since, has no Ed25519 key, so the owner's Client can neither check nor pin them. It still sends them the new epoch, sealed to the X25519 key the server gives for them, and its notice names them, so the owner can compare fingerprints once they have opened EtherPK. The Client tells the owner after the new epoch starts rather than asking first, because asking would keep the graph on the key the former member holds until the owner answered.
An owner can also start a new epoch at any time with Rotate key.
What the server can see
The server sees what it needs to store your data and to decide who can reach it:
- Your account. Your email address, your sign-in records and the network address of each request.
- Graphs. Random ids, the owner, the members and their roles, each invite with its sender, recipient and date, and the current epoch number.
- Documents. Random ids, the size and time of every encrypted update and snapshot, the epoch that encrypted each one, and when a document is deleted.
- Activity. When each device is connected, and which documents it has open, because the server routes edits and cursors by document id.
- Files. Random ids, the exact size, the number of chunks, the time of the upload and the duplicate token.
- Keys. Your identity public keys, your encrypted vault, sealed invites and sealed copies of Graph Keys with their signatures, and device approval requests with their one-time public keys.
The server cannot see the text or the names of your documents, the name or settings of a graph, the names or contents of files, or any key that decrypts them.
Staying safe
Your notes are encrypted on your device. These habits keep them safe:
- Compare security fingerprints. When you start sharing a graph with someone, compare fingerprints with them in person or on a call. When you add a device, check that the approval code is the same on both screens. EtherPK warns you if a person's keys change after you have compared them, and shows anyone you have not compared yet as Unverified (Sharing a graph).
- Protect your sign-in and access tokens. Anyone who can sign in as you can use Reset Encryption Keys, which deletes the graphs you own. An access token cannot reset your Encryption Keys, but an account-wide one still reaches your account's encrypted data. Turn on two-factor authentication, and keep access tokens as safe as your password.
- Keep your Recovery Code safe. If you lose the code and every unlocked device, nobody can decrypt your synced notes, EtherPK included (Recovery Code And Device Approval). If someone may have seen the code, use Replace Encryption Keys, which changes the vault key as well as the code (Replacing your Encryption Keys).
- Keep your devices secure. EtherPK keeps a copy of your synced graphs on each device so that you can work with them. A device passcode protects the Encryption Keys it keeps there. Lock your devices, and use EtherPK only on computers you trust (Encryption Keys and data on your devices).
- Open EtherPK from its own address. The Client runs in your browser from the site you open, such as app.etherpk.com. It runs only its own scripts, and in browsers that support Trusted Types, text from your documents cannot run as script. Its source code is public, and you can run your own copy (Running EtherPK On Your Own Computer).
- Remove people you no longer share with. When the owner removes someone, EtherPK starts a new epoch at once. When someone leaves, the owner's EtherPK starts one the next time it is unlocked. Only current members get the new keys (Membership changes).
- Act on a key warning. If EtherPK says the server publishes a security key for you that is not yours, do not send or accept invites on that server until it is cleared (Troubleshooting).
Cryptographic details
Algorithms
| Use | Algorithm | Parameters |
|---|---|---|
| Encrypting content and wrapping keys | AES-256-GCM | 256-bit key, a random 96-bit nonce for each message, 128-bit tag |
| Key agreement in sealed boxes, device approval and the identity proof | X25519 | 32-byte keys |
| Signatures | Ed25519 | 32-byte keys, 64-byte signatures, strict verification (RFC 8032) |
| Deriving keys from random secrets | HKDF-SHA-256 | See Key derivations |
| Stretching a protection passphrase or a device passcode | Argon2id | 19,456 KiB of memory, 2 passes, parallelism 1, 16-byte salt, 32-byte output |
| Duplicate file tokens and the identity proof | HMAC-SHA-256 | 256-bit key |
| Fingerprints, codes and commitments | SHA-256 | See Codes and fingerprints |
The Client uses the browser's Web Crypto API for AES-256-GCM, HKDF, HMAC and SHA-256,
@noble/curves for X25519 and Ed25519, and @noble/hashes for Argon2id. Every random value comes
from crypto.getRandomValues. The Headless Client runs the same code on Node.js. The Sync Server
checks signatures with @noble/curves under the same strict rules as the Client, so both accept
exactly the same signatures. It checks the identity proof with Node.js's own X25519, HKDF and
HMAC.
The Argon2id defaults are the minimum that OWASP recommends, and a record keeps the parameters it was made with. The Client refuses a record whose parameters are outside 8 MiB to 1 GiB of memory, 1 to 32 passes, parallelism 1 to 16, or a salt of 16 to 64 bytes.
Key derivations
Every HKDF call uses SHA-256 and produces 256 bits. Every info value is UTF-8 text, and so is every salt except the sealed box salt, the device approval salt and the identity proof salt.
- Vault wrap key. HKDF over the 16 bytes of the Recovery Code, with the salt
etherpk-vault-v1and the infovault-wrap. - Sealed box key. HKDF over the X25519 shared secret, with the one-time public key followed by
the recipient's public key as the salt, and the info
etherpk/sealed/v1. - Device approval key. HKDF over the X25519 shared secret of the two devices' one-time keys,
with the 32-byte approval transcript as the salt, and the info
etherpk/device-approval/reply/v2. An all-zero shared secret is refused. - File duplicate secret. HKDF over the graph's newest epoch key, with the graph id as the salt
and the info
etherpk/asset-dedup/v1. - Identity proof key. HKDF over the X25519 shared secret of your X25519 identity key and the
key the server offers, with the offered key followed by your X25519 public key as the salt, and
the info
etherpk/identity-proof/v1. The proof is an HMAC-SHA-256 under it over the key write transcript. An all-zero shared secret is refused. The server derives the key it offers from its own secret with HKDF, with the infoetherpk/identity-proof-key/v1|followed by your account id, so it stores nothing for it. - Passkey wrap key. HKDF over the PRF output, with the salt
etherpk-protection-device-v1and the infoprotection-device-wrap. The PRF input for a graph is the UTF-8 textetherpk:protection-prf:v1:followed by the graph id. - Passphrase wrap key and device passcode key. Argon2id over the UTF-8 passphrase or passcode, with the record's salt and parameters. The output is 32 bytes.
A sealed box works in this order:
- The sender generates a one-time X25519 key pair.
- It calculates the shared secret from the one-time private key and the recipient's public key.
- It derives the sealed box key from the shared secret with HKDF, as in the list above.
- It encrypts the message with AES-256-GCM under that key, with a new random nonce, and puts the one-time public key in the header.
Signed transcripts
Every signature covers a transcript: a label, then each field, each preceded by its length as a 4-byte big-endian number. Numbers are written as decimal text. The label makes a signature for one purpose useless for another.
| Transcript | Label | Fields, in order |
|---|---|---|
| Key write | etherpk/key-write/v1 |
Account id, the vault version it replaces, SHA-256 of the vault record, the identity hash of the keys it publishes (empty when it publishes none) |
| Key replacement | etherpk/key-replace/v1 |
Account id, the vault version it replaces, SHA-256 of the vault record, the identity hash of the new keys |
| Invite | etherpk/invite/v1 |
Graph id, inviter id, invitee id, the invitee's identity hash, SHA-256 of the sealed box |
| Graph Key copy | etherpk/key-handout/v1 |
Graph id, epoch, owner id, recipient id, the X25519 key it is sealed to, SHA-256 of the sealed box |
The identity hash is SHA-256 over the text etherpk/identity-fingerprint/v2 followed by the
32-byte X25519 public key and the 32-byte Ed25519 public key. The receiving Client rebuilds every
transcript from what it knows itself, such as its own account id and keys, so a signature made for
another account or another key does not check out.
Associated data
Every AES-256-GCM operation authenticates a UTF-8 label. Each label starts with etherpk:v1,
and the parts that follow are separated by |. The labels, grouped by the key they go with, are
in the list below.
- Under the Graph Key
- Document update or snapshot:
etherpk:v1|update|graph:<graph id>|doc:<document id> - Cursors and selections:
etherpk:v1|presence|graph:<graph id>|doc:<document id> - Copy of the graph name:
etherpk:v1|graph-name|graph:<graph id> - File metadata:
etherpk:v1|asset-meta|graph:<graph id>|id:<file id>
- Document update or snapshot:
- Under a file key
- File chunk:
etherpk:v1|asset|id:<file id>|chunk:<chunk number from 0>
- File chunk:
- For the vault
- Vault contents, under the vault key:
etherpk:v1|vault|v3 - The vault key, under the wrap key:
etherpk:v1|vault-key
- Vault contents, under the vault key:
- In sealed boxes
- Invite:
etherpk:v1|keyring-invite|graph:<graph id> - Graph Key copy:
etherpk:v1|key-handout|graph:<graph id>|epoch:<epoch>|recipient:<account id>
- Invite:
- Under the device approval key
- The vault key:
etherpk:v1|device-approval|v2|approval:<request id>
- The vault key:
- Under the device passcode key
- The check value:
etherpk:v1|device-passcode|check - A vault key or an access token:
etherpk:v1|device-passcode|secret|<id>, where the id is the vault key's storage name, orsync-token:followed by the server's origin
- The check value:
- For protected documents
- The Protection Key, under the passphrase wrap key:
etherpk:v1|protection-key|passphrase - The Protection Key, under the passkey wrap key:
etherpk:v1|protection-key|device - A protected document, under the Protection Key:
etherpk:v1|protected|<write time in ms>|<key fingerprint in base64url>
- The Protection Key, under the passphrase wrap key:
Envelope formats
All encrypted records start with a format version byte and a kind byte. The Client refuses a record with a version or a kind it does not know.
The symmetric envelope holds everything that is encrypted under a shared key:
| Bytes | Field |
|---|---|
| 0 | Format version, 1 |
| 1 | Kind, 1 |
| 2 to 5 | Epoch number, an unsigned 32-bit little-endian integer, or 0 where no epoch applies |
| 6 to 17 | Nonce, 12 bytes |
| 18 onwards | Ciphertext, then the 16-byte tag |
The epoch number is little-endian, unlike the big-endian lengths in the signed transcripts. The server reads it from this header, without decrypting anything, to refuse a write whose stated epoch is different.
The sealed box:
| Bytes | Field |
|---|---|
| 0 | Format version, 1 |
| 1 | Kind, 2 |
| 2 to 33 | One-time X25519 public key |
| 34 to 45 | Nonce, 12 bytes |
| 46 onwards | Ciphertext, then the 16-byte tag |
The vault:
| Bytes | Field |
|---|---|
| 0 | Format version, 1 |
| 1 | Kind, 5 |
| 2 to 3 | Length of the wrapped vault key, an unsigned 16-bit big-endian integer |
| 4 onwards | The vault key, in a symmetric envelope under the wrap key |
| After the wrapped key | The vault contents, in a symmetric envelope under the vault key |
The vault contents are JSON. They hold both identity key pairs, each Graph Key with its epochs,
the protection records of your synced graphs, and your pins: for each person EtherPK has pinned,
the two public keys, the email address, when they were pinned, and whether you compared the
fingerprint. Vaults written by an earlier version of EtherPK have kind 3 and the label
etherpk:v1|vault. The Client still opens them, and writes them as kind 5 the next time it saves.
The protected document envelope:
| Bytes | Field |
|---|---|
| 0 | Format version, 1 |
| 1 | Kind, 4 |
| 2 to 9 | Write time in milliseconds since 1970, an unsigned 64-bit big-endian integer |
| 10 to 17 | Key fingerprint, the first 8 bytes of SHA-256 over etherpk:protection-fingerprint:v1 followed by the key |
| 18 to 21 | Length of the inner envelope, an unsigned 32-bit big-endian integer |
| 22 onwards | A symmetric envelope under the Protection Key |
Two devices that edit the same protected document offline can leave two envelopes in one code block. Every client keeps the envelope with the later write time, so all of them agree without the key.
A value encrypted under a device passcode is stored as the text pc1: followed by a symmetric
envelope in base64url.
Codes and fingerprints
- Recovery Code. 16 random bytes in Crockford base32, an alphabet without I, L, O and U. It is
shown as
EPK1-followed by 26 characters, in groups of 5, 5, 5, 5 and 6. - Security fingerprint. The first 16 bytes of the identity hash (Signed transcripts), which covers both identity public keys. It is shown as 32 hex digits, in eight groups of four.
- Device approval commitment. SHA-256 over the text
etherpk/device-approval/commit/v2followed by the new device's one-time public key. - Device approval code. The first 40 bits of the approval transcript, in Crockford base32. The
transcript is SHA-256 over the label
etherpk/device-approval/transcript/v2, the request id, the commitment and both one-time public keys, each preceded by its length. It is shown as 8 characters, in two groups of four.
Source code
The code that does everything on this page is in the etherpk-client repository:
apps/client/src/lib/cryptoholds the envelopes, sealed boxes, signatures, Graph Keys, the vault, the Recovery Code, the codes and fingerprints, and protected documents.apps/client/src/lib/synccreates keys and handles invites, pins, new Graph Key epochs, device approval, the device passcode and document sync.apps/client/src/lib/storage/serverencrypts, decrypts and checks files.packages/shared/src/sync-identity.tsbuilds the signed transcripts, which the Client and the Sync Server share.packages/shared/src/envelope-header.tswrites and reads the symmetric envelope's header, which the Sync Server reads the epoch from.
To report a security problem, see Help And Community.