Encrypted Drive
A single addressable event kind for a private-by-default file/folder tree ("a drive"). Files and folders are the same kind of event; the node's type, name, parent, and any per-file blob keys live inside NIP-44-encrypted content, so relays and storage servers only ever see ciphertext and never learn the folder structure.
Motivation
Apps that store files on Blossom servers need an index: names, folders, which blob is which, and per-file encryption keys. Keeping that index in plaintext events leaks the whole layout. This NIP puts the index in one encrypted event kind so the tree stays private while still living on ordinary relays.
The drive key (DK)
A drive is unlocked by one 32-byte random key, the DK. The DK is used directly as a NIP-44 v2 conversation key to encrypt every node.
The DK is stored for the owner as a NIP-78 app-data event (kind:30078) with d tag "<app>/drive-key", whose content is a small JSON object {"k":"<hex DK>"}, NIP-44-encrypted to the owner's own pubkey. So the DK never leaves the owner's control, and a signed-in client fetches + unlocks it once per session.
The node event
Each file or folder is one addressable event:
{
"kind": 30630,
"tags": [
["d", "<node id>"], // random 8-byte hex; also how children point to a parent
["client", "<app name>"]
],
"content": "<NIP-44(DK, node_json)>"
}The decrypted content is a JSON object:
{
"type": "file", // or "folder"
"name": "photo.jpg",
"parent": "<parent node id>", // "root" for the top level
"deleted": false,
// files only:
"blobHash": "<sha256 of the stored bytes>",
"size": 12345,
"mime": "image/jpeg",
"servers": ["https://blossom.example"],
// private files only (the stored blob is segmented AES-256-GCM ciphertext):
"blobKey": "<base64 32-byte AES key>",
"encv": 1, // encryption-format version; see Large files
"ox": "<sha256 of the plaintext>" // optional; integrity + dedup
}Large files (private blob encryption)
A private file's blob is not encrypted as one monolithic AEAD. It's encrypted as a sequence of fixed-size segments, each authenticated on its own, so a client encrypts and decrypts one segment at a time with bounded memory (no full-file buffering, no OOM) regardless of file size, and can seek: any segment is decryptable on its own from a Range request over the single blob. The result is still one blob with one sha256, whose content-addressing gives per-file integrity and hash-healing. (Encrypted bytes never dedup, since a fresh key and nonces are used every time, so dedup is not a goal of the blob layout; that's what the plaintext hash ox is for.)
The construction is a STREAM-style segmented AEAD (the same shape age uses). This is format version 1, marked by encv: 1 on the node (absent means 1); the params below are fixed for v1, and a future encv MAY change them (e.g. the segment size) without breaking existing blobs, since each node records its version:
- Segment size: 65536 bytes (64 KiB) of plaintext per segment. Every segment but the last is exactly this size; the last may be shorter (including empty).
- Cipher: AES-256-GCM under the file's 32-byte blobKey.
- Per-segment nonce (12 bytes): an 11-byte big-endian segment counter starting at 0, followed by a 1-byte flag, 0x00 for every segment except the last, 0x01 for the last.
- Segment output: AES-256-GCM-Seal(key, nonce, plaintext_segment) → ciphertext ‖ 16-byte tag. The stored blob is the concatenation of all segment outputs, and its sha256 is the blobHash value.
Why the counter and last-flag:
- The counter ties each segment to its position, so a reordered or swapped segment fails its tag.
- The last-flag marks the final segment, so a truncated tail is detected: the decryptor never reaches a 0x01 segment and errors, instead of silently returning a short file.
Decrypting reads 65536 + 16 bytes per segment (the last is shorter), opens each with the derived nonce, and stops at the 0x01 segment.
A file that fits in one segment is just the N = 1 case (a single 0x01 segment), so small and large files use the exact same format, no special case. No nonce is stored: the segment nonces are derived from the counter, and the per-file blobKey is fresh random 32 bytes, so no (key, nonce) pair is ever reused.
Notes
- Parents are ids, not paths. Moving a folder is one re-publish (change parent); nothing else moves, so folder rename/move is O(1), not an O(N) rewrite of every file beneath it.
- Private files: the blob is the ciphertext; its blobKey is here in the node. See Large files for the encryption format and encv.
- Public files: omit blobKey; blobHash is the sha256 of the plaintext bytes as usual for Blossom.
- ox (private files, optional): the sha256 of the plaintext. For a private file blobHash is the ciphertext hash, which changes on every encryption, so ox is the stable content identity, used to verify plaintext integrity and to skip re-uploading a file already held. Public files omit ox: their blobHash already is the plaintext hash.
- Folders may carry optional display fields (e.g. color, emoji).
- File bytes are stored and retrieved as content-addressed blobs per BUD-01 (GET/HEAD) and BUD-02 (upload/list/delete); the servers field follows BUD-03. The Blossom server stays oblivious: it only ever sees an opaque blob.
- Extra fields: clients MAY include additional application-specific fields, and MUST preserve unknown fields when re-publishing a node, so different clients interoperate without dropping each other's data. The names v and history are reserved (see Versioning).
Deletes
Because relays don't always honor NIP-09, a delete is a tombstone: re-publish the node with "deleted": true and a strictly newer created_at. Clients treat a tombstoned node as gone. A NIP-09 kind:5 MAY also be sent as a best-effort cleanup.
Client behavior
- On load, fetch the owner's kind:30630 events, decrypt each with the DK, and assemble the tree by parent.
- Newest created_at wins per d (addressable-event replacement).
- A node that fails to decrypt is another owner's / another drive's, ignore it.
Versioning (reserved)
This version of the spec stores a single current version per file. The field names v (a per-file version counter) and history (an array of prior full-copy versions, newest-first) are reserved for a future optional versioning extension. Clients that don't implement versioning MUST still preserve these fields if present (see Extra fields), so a versioning client and a non-versioning one interoperate on the current version without data loss.