Protocol · 03

Albums and epochs

An album is a set of people and a numbered sequence of keys. A group key that changes when membership changes is not a new idea, but these rules do not come from an existing specification, so this page explains each rule and the reason for it, following one album: Alice creates Picnic, invites Bob and Carol, and later removes Carol.

Epochs

An epoch is one period of an album's membership, with one album master key (MK): 32 random bytes. Keys are generated independently, so one epoch's key says nothing about another's. Members keep every epoch key they receive, because a photo stays encrypted under the epoch it was uploaded in.

Alice createsmakes MK0Bob joinsgets MK0Carol joinsgets MK0Carol removeduploads pauseAlice commits epoch 1MK1 to Alice and BobEpoch 0 · MK0Epoch 1 · MK1AliceMK0MK0, MK1BobMK0MK0, MK1CarolMK0keeps MK0, never gets MK1
Picnic across two epochs. Joining never starts an epoch; leaving always does.
EventNew epoch?Why
Album createdEpoch 0The album needs its first key
Someone is invitedNoThey receive the existing keys, so they can see the history, which is the point of joining
Someone is removed or leavesYesPhotos from now on must use a key they never had

I chose to rotate only on removal because it is the one event where a new key protects anything. Rotating on every join would cost a handshake per member and hide nothing, since the newcomer receives all the old keys anyway.

One admin

ActionAdminMember
Upload, and delete their own photosYesYes
Invite someoneYesNo
Remove someone elseYesNo
LeaveNo, the admin deletes the album insteadYes
Start a new epochYesNo

Every epoch change is signed by the admin's identity key, so the admin is the album's single source of authority. The database has a co-admin role, but it is switched off. Several signers raise questions about who wins a conflict and who takes over when someone leaves, and I have not settled those, so for now each album has exactly one signer. Co-admins, and handing an album to someone else so the admin can leave, are planned.

Creating an album

  1. Alice's phone creates Picnic on the server with a placeholder title, because no key exists yet to encrypt the real one.
  2. It generates MK0.
  3. It fetches its own prekey bundle and runs the normal handshake with itself, producing a wrap addressed to its own member token.
  4. It signs that wrap and the epoch change and sends epoch 0.
  5. Once the server accepts it, the phone installs MK0, encrypts the real title under it and replaces the placeholder.

Wrapping the first key to yourself is not a backup: a reinstalled app has new keys and cannot open it. It exists so that epoch 0 has exactly the same shape as every later epoch, and the server checks it the same way.

Signed epoch changes

To start epoch N, the admin's phone makes a fresh MK, runs one handshake per remaining member, and sends every wrap in one request, with a signature over the whole change:

member_set_hash = SHA256(u32_be(count) || member tokens, sorted)

wraps_hash      = SHA256(u32_be(count) || for each wrap, sorted by token:
                    token (32) || ek_pub (32) || u32_be(opk_index or 0xFFFFFFFF)
                    || u32_be(len) || wrap || u32_be(len) || sender_sig)

envelope_sig    = Ed25519(IK_admin, "epoch-set-v1" || album_id (16)
                    || u32_be(N) || member_set_hash || wraps_hash)

The server takes a lock on the album row, then checks:

  • the caller is still the album's active admin,
  • N is exactly the current epoch plus one,
  • the tokens are exactly the active members, with no duplicates,
  • both hashes match the request, and envelope_sig verifies under the admin's IK.

Only then does it store the epoch and every wrap in one transaction and notify members, who each fetch their own wrap. Removals, epoch changes, invites and uploads all take the same album lock, so none of them can interleave.

Why sign the whole set rather than just each wrap: a per wrap signature proves who sent one key, but not who else got it. Signing the member list and every wrap together means a server cannot quietly add a recipient, drop one, or swap a wrap without breaking the signature.

Inviting with history

Bob shares his Miuchio ID with Alice. Picnic is at epoch 0.

  1. Alice's phone fetches and checks Bob's prekey bundle.
  2. It runs one handshake with Bob.
  3. It wraps every MK it holds, from epoch 0 to the current one, under that single secret. Each wrap has a fresh nonce, its own epoch in the associated data and its own signature.
  4. The server, under the album lock, checks that Alice is still the admin, neither account is being deleted, the wraps cover exactly epochs 0 to current, no new epoch is owed, and the album has fewer than 10 members. Then it adds Bob and stores the wraps in one transaction.
  5. Bob's phone installs every epoch from 0 upward and sends a signed receipt: Ed25519(IK_Bob, "join-complete-v1" || album_id || u32_be(epoch) || ek_pub). The server verifies it and records the newest epoch Bob has confirmed.

One handshake for the whole history is deliberate. The invitation grants all of it anyway, so separate handshakes would protect nothing extra, and one handshake uses only one of Bob's one-time prekeys.

If Carol is removed and later invited back, she gets her old member token again, because signed history already refers to it. She rejoins as a plain member, her old wraps are replaced, and her old encrypted name is dropped because it was sealed under a key she may no longer hold.

Removing someone

Alice removes Carol. The order is always revoke, then rotate.

  1. In one locked transaction the server marks Carol revoked and sets the album's rotation_required flag. Carol loses access to every album request at once. Photo download links she was handed before that keep working until they expire, at most 30 minutes later, and they only reach photos she could already open.
  2. From that moment uploads to Picnic are refused. An upload already in progress is refused when it tries to confirm.
  3. Alice's phone creates epoch 1 for Alice and Bob, wrapping only to identity keys it has already pinned for them.
  4. Committing epoch 1 clears the flag, and uploads continue under MK1.

The freeze is the important part. Between steps 1 and 3 the album's current key is still MK0, which Carol holds. Without the freeze, a photo Bob uploads in that gap would be encrypted under a key Carol has. The gap can last seconds, or hours if Alice's phone is offline.

Why the flag is stored instead of computed: an earlier version compared the current epoch's recipients with the active members. When a departed account is deleted, its wraps are deleted too, the two lists match again, and the album looks settled while that person still holds the current key. A safety rule inferred from data can be undone by an unrelated feature that deletes data. The comparison is still checked as well, but it can no longer switch the freeze off. The full story is in Removing someone without leaking the next photo.

Leaving works the same way, except Carol revokes herself and her phone deletes its copy of the album. Alice's phone still has to create the next epoch, and it tries on launch, on reconnect, when it hears about a removal, and when one of its own uploads is refused for this reason.

When the admin is away

Today only the admin's phone can create a new epoch. If someone leaves while that phone is offline, uploads to the album stay paused until it comes back, and if it never comes back, they stay paused. I chose a pause over an album that keeps using a key a removed person holds, but a pause is still a limit.

What I'm working on next is letting any remaining member create the new epoch. Whichever member opens the album first makes the new key and delivers it to everyone else, so an album never waits on one phone. Two parties can never do it: the person who left, because they would know the new key, and the server, because it could keep a copy.

This needs rules the single admin design does not have: which epoch wins when two members try at once, and how each phone decides that a member may sign an epoch change. I am designing those now, and this page will describe them once they are built.

Catching up after being offline

Realtime events are only hints; a phone that was asleep misses them. On launch and reconnect the app asks for each album's current epoch, compares it with the keys it holds, and installs every missing epoch oldest first. Work for one album runs in order, so keys never install out of sequence.

If an epoch cannot be installed, the album is marked blocked with one of these reasons:

  • the signer's identity key does not match the one pinned for them,
  • the signer is unknown on an album this phone already has keys for,
  • this phone's own member token carries someone else's identity key,
  • the wrap is still missing after retries,
  • the signature or decryption failed,
  • the one-time prekey the wrap names is no longer on this phone,
  • the key conflicts with one already installed.

Deleting an account

SituationWhat happens to the album
You are the only memberIt is deleted
You are the admin and others remainIt is deleted for everyone, after you confirm each one
You are a memberYou leave, your photos are deleted, and a new epoch is owed

The server accepts the request, ends every session, and a background job removes the account one album at a time, each step its own transaction so a crash resumes where it stopped. The phone sends a random receipt with the request and keeps it in a small file. If the response is lost, the phone asks whether that receipt was accepted before wiping itself, so it never wipes for a deletion that did not happen.