# CipherFS vault format

This document specifies how a CipherFS vault and a directory store are laid out on disk and how to decrypt them, written for someone who has the files and a password or recovery key but not the software. It covers wire format 1 and database schema 1. Every vault keeps a byte-identical copy of this file in its root, next to `format-vectors/` (public vectors with expected outputs) and `recover.py` (the reference decryptor, whose structure follows the sections below).

Conventions: `‖` is concatenation; `u32be`/`u64be` are big-endian and `u32le`/`u64le`/`u16le` little-endian unsigned integers; byte ranges are `[start, end)`; strings are UTF-8 without a terminator; hex values are lower case. "Message" means a protobuf message of `cipherfs.proto` (Appendix A).

## 1. Constants

| Name | Value |
| --- | --- |
| Wire format | `1` |
| Database schema | `1` |
| Segment magic | `CFSSEG02` |
| Segment header bytes | `124` |
| Snapshot magic | `CFSSNP03` |
| Snapshot header bytes | `132` |
| Maximum segment bytes | `33554432` |
| Maximum unit plaintext bytes | `16777216` |
| Maximum path bytes | `4096` |
| Local packed ciphertext limit bytes | `65552` |
| Argon2id default memory KiB | `65536` |
| Argon2id default iterations | `3` |
| Argon2id default parallelism | `4` |
| Argon2id output bytes | `32` |
| Argon2id version | `19` |
| Argon2id maximum memory KiB | `262144` |
| Argon2id maximum iterations | `10` |
| Argon2id maximum parallelism | `16` |
| FastCDC default minimum bytes | `1048576` |
| FastCDC default average bytes | `4194304` |
| FastCDC default maximum bytes | `8388608` |
| Base58 alphabet | `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz` |
| Recovery key alphabet | `0123456789ABCDEFGHJKMNPQRSTVWXYZ` |

A reader rejects a segment or snapshot whose authenticated format is above 1 and a database whose schema is not 1. Chunking parameters only describe the writer; readers follow the stored unit list.

## 2. Encodings

### 2.1 Protobuf

All persistent messages are proto3 messages of package `cipherfs.v1` (Appendix A), encoded with the standard protobuf wire format. Readers skip unknown fields. Some bodies are authenticated as the exact bytes their author encoded (`HashedBytes`, `SignedBytes`, `IndexRecord.body`); such a body is hashed or verified as stored and only then decoded, never re-encoded.

Typed values: `Digest` is `(algorithm, value)` with BLAKE3 = 1 and a 32-byte value; `PublicKey` uses Ed25519 = 1 or X25519 = 2 with 32 bytes; `Signature` uses Ed25519 = 1 with 64 bytes. `AeadCiphertext` is `(algorithm, nonce, ciphertext ‖ 16-byte tag)`; its AAD is defined where it is used. `SealedBox` is an ephemeral X25519 key and an `AeadCiphertext` (section 6.3).

### 2.2 Text identities

Public text uses Bitcoin base58 (alphabet in section 1) without a checksum; each leading zero byte encodes as one leading `1`. Decoding must produce exactly the expected length and re-encode to the same text. Digests and keys carry their algorithm as a prefix: `b3.<base58>` (VersionId, key epoch, object hashes), `ed25519.<base58>` (DeviceId, signatures), `x25519.<base58>`. FileId, SubtreeId, StoreId and VaultId are 16 random bytes in unprefixed base58. The all-zero key epoch is the master key's epoch.

### 2.3 Recovery key text

A recovery key is 32 secret bytes `R`. Its text is `R ‖ derive_key("cipherfs recovery key checksum v1", R)[0..2]` (34 bytes, 272 bits) followed by 3 zero bits, cut into 55 five-bit symbols from the most significant bit, each mapped through the recovery key alphabet (section 1), in groups of five joined by `-`. Parsing ignores `-` and whitespace, is case-insensitive, reads `I` and `L` as `1` and `O` as `0`, and requires the checksum and the zero padding bits.

## 3. Algorithms

### 3.1 Registry

| Family | ID | Text | Standard |
| --- | --- | --- | --- |
| hash | `1` | `b3` | BLAKE3 (BLAKE3 specification, 2020; 32-byte output) |
| hash | `2` | `sha256` | SHA-256, FIPS 180-4 (registered; not used for identities or content) |
| hash | `3` | `sha512` | SHA-512, FIPS 180-4 (registered; not used for identities or content) |
| aead | `1` | `xchacha20poly1305` | XChaCha20-Poly1305 (draft-irtf-cfrg-xchacha; ChaCha20-Poly1305 of RFC 8439 with HChaCha20), 24-byte nonce, 16-byte tag |
| aead | `2` | `aes256gcm` | AES-256-GCM, NIST SP 800-38D, 12-byte nonce, 16-byte tag |
| aead | `3` | `chacha20poly1305` | ChaCha20-Poly1305, RFC 8439, 12-byte nonce, 16-byte tag |
| signature | `1` | `ed25519` | Ed25519, RFC 8032 (pure, no context) |
| public-key | `1` | `ed25519` | Ed25519 public key, RFC 8032 |
| public-key | `2` | `x25519` | X25519, RFC 7748 |
| kdf | `1` | `argon2id` | Argon2id version 0x13, RFC 9106 |
| chunking | `1` | `fastcdc-2020` | FastCDC (Xia et al., 2020); writer only |
| chunking | `2` | `fixed` | Fixed-size boundaries; writer only |

Local values, index containers, sealed boxes and key wraps use XChaCha20-Poly1305 only; content may use any of the three AEADs, as recorded per file. Format 1 does not compress data and uses no HKDF.

### 3.2 BLAKE3 modes and labels

Key derivation uses BLAKE3 in its three standard modes: `hash(data)`, `keyed_hash(key32, data)` and `derive_key(context, material)`, all with 32-byte output. Every context, AAD prefix and domain string of format 1:

| Label | Use |
| --- | --- |
| `cipherfs local seal v6` | `derive_key`: local value seal key from the local storage key (5.4) |
| `cipherfs local lookup v6` | `derive_key`: local lookup key from the local storage key (5.4) |
| `cipherfs index v6` | `derive_key`: index key of an epoch (6.7) |
| `cipherfs content v6` | `derive_key`: content base key (6.8) |
| `cipherfs epoch identity v1` | `derive_key`: identity of a random epoch key (6.6) |
| `cipherfs master binding v1` | `derive_key`: genesis commitment to the master key (6.6) |
| `cipherfs device kx v1` | `derive_key`: device X25519 secret from the device seed (6.5) |
| `cipherfs seal v1` | `derive_key`: sealed-box key (6.3) |
| `cipherfs recovery key checksum v1` | `derive_key`: recovery key checksum (2.3) |
| `cipherfs recovery enrollment v1` | `derive_key`: recovery signing seed from the recovery key; also the domain of `RecoveryEnrollmentBody` |
| `cipherfs version id v1` | `derive_key`: VersionId of a `VersionRecordBody` (6.9) |
| `cipherfs retirement id v1` | `derive_key`: RetirementId of a `RetirementBody` |
| `cipherfs snapshot frontier v1` | `derive_key`: snapshot frontier commitment (7.2) |
| `cipherfs pack upload inventory v1` | `derive_key`: upload-inventory commitment (7.2) |
| `cipherfs local secrets v1` | AAD context of the password and recovery wraps (6.1, 6.2) |
| `cipherfs epoch key v1` | AAD prefix of a sealed epoch key (6.6) |
| `cipherfs-file-data-v6` | AAD prefix of a content unit (6.8) |
| `subtree` | `keyed_hash` input prefix of a subtree key (6.8) |
| `cipherfs device enrollment v1` | Domain of `EnrollmentRequestBody` |
| `cipherfs recovery envelope v1` | Domain of `RecoveryEnvelopeBody` (7.3) |
| `path` | Lookup domain of `paths` keys (5.4) |
| `dir` | Lookup domain of a parent directory in `children` keys |
| `child` | Lookup domain of a child in `children` keys |
| `local-dir` | Lookup domain of `local_dirs` keys |

Pairing and peer transport use further labels outside this at-rest format.

## 4. Directory layouts

### 4.1 Vault directory

| Path | Content |
| --- | --- |
| `metadata.redb` | Local index database (5.2) |
| `storage/refcounts.redb` | Local content database (5.3) |
| `storage/chunks/<hh>/<hex>` | One unit ciphertext; `<hex>` is the 64-character lower-case hex BLAKE3 of the file, `<hh>` its first two characters |
| `storage/ciphertext_packs/pack_<NNNNNN>.bin` | Concatenated unit ciphertexts without headers; `NNNNNN` is the decimal pack ID, zero-padded to six digits |
| `init-info.json` | Public trust anchor and configured stores, written by the command-line tool |
| `FORMAT.md`, `format-vectors/`, `recover.py` | This document, vectors and the reference decryptor (absent when switched off at creation) |
| `format-history/` | Earlier specifications after a format upgrade (section 11) |

A unit ciphertext of at most 65552 bytes is stored in a local pack, a larger one as a chunk file. Ranges of reclaimed pack bytes may read as zeros; they are no longer referenced.

### 4.2 Directory store

A directory store holds immutable objects whose names are also their relative paths:

| Object | Name |
| --- | --- |
| Vault marker (empty) | `v/<VaultId>` |
| Store marker (empty) | `s/<StoreId>` |
| Segment (7.1) | `i/ed25519.<device>/<seq:016x>-b3.<hash>-<length>` |
| Pack | `p/<first two characters of the hash value>/b3.<hash>-<length>` |
| Snapshot (7.2) | `n/ed25519.<publisher>-b3.<hash>-<length>` |
| Upload inventory (7.2) | `u/b3.<hash>-<length>` |
| Recovery object (7.3) | `r/b3.<epoch>-b3.<hash>-<length>` |

`<hash>` is the BLAKE3 of the complete object bytes and `<length>` their decimal byte count without leading zeros; `seq` is 16 lower-case hex digits. A name longer than 256 bytes, with another prefix, case or algorithm, is not an object; readers ignore it, as they ignore `tmp/`, names beginning with `.` and the description files. A pack is the concatenation of unit ciphertexts; `PackLocator` records (section 8) give each unit's offset and length.

## 5. Local databases

### 5.1 redb file layout

Both databases are redb 2 files (file format 2 or 3). All integers are little-endian.

- Header: bytes `[0, 9)` are `72 65 64 62 1a 0a a9 0d 0a`; byte 9 is the god byte (bit 0 selects the primary commit slot, bit 1 means recovery is required, bit 2 means two-phase commit); `u32le` page size at 12, region header pages at 16, region data pages at 20.
- Commit slots: slot 0 at byte 64, slot 1 at byte 192, 128 bytes each. Slot byte 0 is the file format version; byte 1 is non-zero when a user root exists; bytes `[8, 16)` hold the user root page number, followed by its checksum (16 bytes) and length (8 bytes).
- Page number `n` (`u64le`): order `o = n >> 59`, region `r = (n >> 20) & 0xFFFFF`, index `i = n & (0xFFFFF >> o)`. The page starts at `page_size + r × (header_pages + data_pages) × page_size + header_pages × page_size + i × (page_size << o)` and is `page_size << o` bytes long.
- B-tree page: byte 0 is the type (1 leaf, 2 branch), `u16le` count at 2. A leaf with `n` entries has `n` `u32le` key ends at 4, then `n` `u32le` value ends, then the keys (the first starts at `4 + 8n`), then the values (the first starts at the last key end); ends are offsets within the page. A branch with `k` keys has `k + 1` 16-byte child checksums at 8, then `k + 1` `u64le` child page numbers; a reader visits all children in order.
- The user root is a B-tree from table name (UTF-8) to a table definition: byte 0 table type, `u64le` length at 1, byte 9 non-zero when the table has a root, root page number `u64le` at 10. Every CipherFS table maps variable-length byte keys to variable-length byte values.

Read the primary slot of a database that CipherFS closed normally. If the process was interrupted, the other slot may hold the last complete commit. Checksums are XXH3-128 and need not be verified: every value a reader uses is authenticated by AEAD, a hash or a signature.

### 5.2 `metadata.redb`

| Table | Sealed | Key → value |
| --- | --- | --- |
| `config` | no | `"password"` → `VaultConfig` |
| `secrets` | yes | `"identity"` → `LocalIdentity` |
| `epoch_keys` | yes | epoch (32 bytes) → `EpochKeyMaterial` |
| `files` | yes | FileId (16) → `FileState` |
| `paths` | yes | `lookup("path", path)` → `PathEntry` |
| `children` | yes | `lookup("dir", parent) ‖ lookup("child", ·)` → `ChildEntry` |
| `versions` | yes | VersionId (32) → `StoredVersion` |
| `trash` | yes | FileId → `TrashRow` |
| `segments` | no | device (32) ‖ `u64be` seq → `BytesValue` holding the segment wire bytes |
| `segment_meta` | no | device ‖ `u64be` seq → `SegmentMeta` |
| `seen_frontier` | no | device → `FrontierEntry` |
| `members` | yes | device → `MemberState` |
| `local_dirs` | yes | `lookup("local-dir", path)` → `StringValue` (a device-local empty directory) |
| `unpublished` | no | `u64be` seq → `Empty` |
| `store_marks` | no | store ‖ device → `Uint64Value` |
| `settings` | mixed | `"schema"` → `DatabaseSchema` (plain); other rows sealed |
| `pack_locators` | no | unit hash (32) ‖ pack hash (32) → `PackLocatorValue` |
| `store_packs` | no | store ‖ pack hash → `Uint64Value` (pack length) |
| `sync_positions` | yes | device → `StoredSyncPosition` |
| `content_marks` | no | device → `Uint64Value` |
| `seg_versions` | no | device ‖ `u64be` seq → `VersionIdList` |
| `content_stores` | no | store → `Empty` |
| `retirements` | yes | RetirementId → `RetirementStateRow` |
| `retirement_cancels` | yes | RetirementId → `CancellationList` |
| `retired_versions` | yes | VersionId → `RetiredVersion` |
| `retention_policies` | yes | device ‖ `u64be` seq → `PolicyPublication` |
| `store_gc` | yes | store ‖ object name → `StoreGcObservation` |
| `index_events` | yes | device ‖ `u64be` seq ‖ ordinal → `IndexEvent` |
| `retirement_rejected` | yes | RetirementId → `VersionIdList` |
| `replay_versions` | yes | VersionId → `StoredVersion` |
| `authority_segments` | no | device ‖ `u64be` seq → `BytesValue` |

A reader needs `config`, `secrets`, `epoch_keys`, `paths`, `files`, `versions`, `segment_meta`, `segments` and the `"schema"` row of `settings`, which must decode to schema 1.

### 5.3 `storage/refcounts.redb`

| Table | Key → value |
| --- | --- |
| `ref_counts` | unit hash → `CasReferenceCount` |
| `packed_chunks` | unit hash → `CasPackedLocation` (pack ID, offset, length) |
| `access_times` | unit hash → `CasAccessTime` |
| `settings` | `"schema"` → `DatabaseSchema` |

A unit with a `packed_chunks` row lies at `[offset, offset + length)` of `ciphertext_packs/pack_<NNNNNN>.bin`; otherwise it is the chunk file named by its hash. Either way its bytes must hash to the unit hash and have the recorded length; a missing object means this device does not hold the content.

### 5.4 Sealed values and lookups

From the local storage key `L` (section 6.1): `seal_key = derive_key("cipherfs local seal v6", L)` and `lookup_key = derive_key("cipherfs local lookup v6", L)`.

A sealed value is an `AeadCiphertext` with algorithm 1, opened with `seal_key` and AAD `table_name ‖ 0x00 ‖ row_key ‖ u32be(1) ‖ nonce`. The plaintext is the message named in 5.2.

`lookup(domain, identity) = keyed_hash(lookup_key, u64le(len(domain)) ‖ domain ‖ identity)`. Paths are canonical: relative, `/`-separated, no empty, `.` or `..` component, no NUL, at most 4096 bytes.

## 6. Key chain

### 6.1 Password

1. Read `config["password"]` as `VaultConfig`. Require `kdf.algorithm = 1`, Argon2id parameters with `output_length_bytes = 32`, `version = 0x13`, `memory_cost_bytes` divisible by 1024 and within the maxima of section 1, and `password_wrap.algorithm = 1` with a 24-byte nonce.
2. `salt` is a PHC "B64" string (standard base64 alphabet, no padding); decode it to bytes.
3. `KEK = Argon2id(password = UTF-8 password, salt, memory = memory_cost_bytes / 1024 KiB, iterations, parallelism, output 32 bytes, version 0x13)`, with no secret key and no associated data (RFC 9106).
4. Open `password_wrap` with `KEK` and AAD `"cipherfs local secrets v1" ‖ u32be(1) ‖ nonce`. The plaintext is `LocalSecretsBody`: the 32-byte device seed and the 32-byte local storage key `L`. Failure means a wrong password.

### 6.2 Recovery key

`VaultConfig.recovery_pk` is the X25519 public key of the recovery key `R` (section 2.3) and `recovery_wrap` a sealed box (6.3) to it with context `"cipherfs local secrets v1"`. Its plaintext is the same `LocalSecretsBody`. A recovery key rotation replaces both fields.

### 6.3 Sealed boxes

To open `SealedBox(ephemeral E, AeadCiphertext(1, nonce, c))` with secret `s` and context `C`: `P = X25519(s, 9)`, `shared = X25519(s, E)` (RFC 7748; reject all zeros), `key = derive_key("cipherfs seal v1", shared ‖ E ‖ P)`, and open `c` with XChaCha20-Poly1305, `key`, `nonce` and AAD `C ‖ u32be(2) ‖ E ‖ u32be(1) ‖ nonce`.

### 6.4 Local secrets

`secrets["identity"]` (`LocalIdentity`) must open with the seal key of `L` and hold the same device seed; it also names the VaultId.

### 6.5 Device keys

The device seed is an Ed25519 secret key (RFC 8032); its public key is the DeviceId. The device's X25519 secret is `derive_key("cipherfs device kx v1", seed)`; member records publish its public key.

### 6.6 Epoch keys

The master key is the key of epoch zero (32 zero bytes); genesis commits to it as `derive_key("cipherfs master binding v1", master)`. Every later epoch key `K` is random and identified by `derive_key("cipherfs epoch identity v1", K)`. A device holds its keys in `epoch_keys`: each sealed `EpochKeyMaterial` names the epoch, which must equal the row key and, except for epoch zero, the derived identity of the key.

Epoch keys sealed to a member or to the recovery key use sealed boxes with context `"cipherfs epoch key v1" ‖ VaultId ‖ epoch`. `EpochKeys.prev_key_under_new` is an `AeadCiphertext` holding an `EpochKeyBundle` under the new key, with AAD `"cipherfs epoch key v1" ‖ VaultId ‖ new epoch ‖ u32be(1) ‖ nonce`.

### 6.7 Index key

`index_key = derive_key("cipherfs index v6", K)` for the epoch key `K` named in a container header.

### 6.8 Content keys

For a `ContentBody` with suite `(aead, hash)`, SubtreeId `S` (currently 16 zero bytes) and content ID `I` (32 bytes), and a unit encrypted under epoch key `K`:

```
subtree_key = keyed_hash(K, "subtree" ‖ S)
base        = derive_key("cipherfs content v6", K ‖ subtree_key)
content_key = keyed_hash(base, u8(aead) ‖ u8(hash) ‖ I)
unit_aad    = "cipherfs-file-data-v6" ‖ u8(aead) ‖ u8(hash) ‖ I ‖ u64le(unit index) ‖ u64le(unit plaintext length)
```

Unit `n` (counting from 0) decrypts with the suite's AEAD, `content_key`, the unit nonce and `unit_aad`; its ciphertext is `ciphertext_length_bytes = plaintext_length_bytes + 16` bytes whose BLAKE3 is the unit hash. Each unit names its own epoch.

### 6.9 Identities

`VersionId = derive_key("cipherfs version id v1", VersionRecordBody bytes)`. Segment and object hashes are plain BLAKE3 of the complete wire bytes.

## 7. Containers

### 7.1 `CFSSEG02` segment

| Bytes | Field |
| --- | --- |
| `[0, 8)` | `CFSSEG02` |
| `[8, 12)` | format, `u32be` |
| `[12, 16)` | public-key algorithm, `u32be` (1) |
| `[16, 48)` | author DeviceId |
| `[48, 56)` | sequence, `u64be`, from 1 |
| `[56, 60)` | epoch hash algorithm, `u32be` (1) |
| `[60, 92)` | key epoch |
| `[92, 96)` | AEAD algorithm, `u32be` (1) |
| `[96, 100)` | signature algorithm, `u32be` (1) |
| `[100, 124)` | nonce |
| `[124, len − 64)` | ciphertext of a `SegmentPayload`, including the tag |
| `[len − 64, len)` | Ed25519 signature |

The signature covers bytes `[0, len − 64)` and is verified with the header's DeviceId before anything else. The AEAD AAD is the 124 header bytes, the key is the index key of the header's epoch. The payload repeats VaultId, device, sequence and epoch, which must match; `prev` chains the author's previous segment hash; `records` carry original `IndexRecordBody` bytes in order. A segment is at most 33554432 bytes.

### 7.2 `CFSSNP03` snapshot and upload inventory

| Bytes | Field |
| --- | --- |
| `[0, 8)` | `CFSSNP03` |
| `[8, 12)` | format, `u32be` |
| `[12, 16)` | public-key algorithm, `u32be` (1) |
| `[16, 48)` | publisher DeviceId |
| `[48, 52)` | epoch hash algorithm, `u32be` (1) |
| `[52, 84)` | key epoch |
| `[84, 92)` | sum of covered sequence numbers, `u64be` |
| `[92, 100)` | covered segment bytes, `u64be` |
| `[100, 104)` | AEAD algorithm, `u32be` (1) |
| `[104, 108)` | signature algorithm, `u32be` (1) |
| `[108, 132)` | nonce |
| `[132, 140)` | authority proof length `a`, `u64be` |
| `[140, 140 + a)` | original `AuthorityProof` bytes (possibly empty) |
| then | ciphertext including the tag |
| `[len − 64, len)` | Ed25519 signature |

The signature covers everything before it; the AAD is bytes `[0, 140 + a)`. A snapshot's plaintext is a `SnapshotPayload`, whose `frontier_hash` is `derive_key("cipherfs snapshot frontier v1", frontier.body)`. Upload inventories use the same envelope with a `PackUploadInventory` payload, whose commitment is `derive_key("cipherfs pack upload inventory v1", committed.body)`. Snapshots accelerate other devices and are not needed to read a vault directory.

### 7.3 Recovery object

`r/` objects hold a `RecoveryEnvelope`: a `SignedBytes` whose body is a `RecoveryEnvelopeBody` with domain `"cipherfs recovery envelope v1"`, signed (Ed25519 over the body bytes) by the publisher it names. `sealed_keys` is a sealed box to the recovery key with context `"cipherfs epoch key v1" ‖ VaultId ‖ epoch`; its plaintext is an `EpochKeyBundle` of all epoch keys the publisher held. `authority_segments` carries the original segments that prove the publisher's membership from the anchored genesis.

## 8. Records and the shown file

Each `IndexRecordBody` holds exactly one variant. A version (`VersionEnvelope`) carries its VersionId and the original `VersionRecordBody`: FileId, parent VersionIds, path and either `ContentBody` or a tombstone. The heads of a file are its versions that no known version names as a parent. `PackLocator` records map unit hashes to ranges of a store pack. Other variants carry membership, keys, retention and synchronization state.

When a vault materializes its index, `files[FileId]` names the file's current version, and `paths[lookup("path", p)]` lists the FileIds whose current, non-deleted version has path `p`. The shown version among several candidates follows this rule:

1. Discard a candidate if another candidate is causally later: the same author with a higher sequence, or a later segment whose `SegmentMeta.deps` (in `segment_meta`, keyed by author and sequence) contains the earlier author with a sequence at least the earlier one's.
2. Among the rest, content wins over a tombstone, then the larger publication time (`StoredVersion.time_epoch_ms`), then the larger VersionId bytes.

## 9. Listing and exporting a vault directory

1. Open `metadata.redb` (5.1) and require schema 1 (5.2).
2. Unlock with the password (6.1) or the recovery key (6.2) to obtain the device seed and the local storage key `L`; check `secrets["identity"]` (6.4).
3. Derive the seal and lookup keys (5.4) and load all `epoch_keys` (6.6).
4. For every `paths` row: decode the `PathEntry`; for each FileId read `files` and the current version from `versions`; check that the row key equals `lookup("path", path)` and that each `StoredVersion.original_record` holds a version whose identity matches (6.9). Select the shown version (8).
5. Optionally authenticate each shown version against its carrier: the `segments` row for its author and sequence must verify and decrypt (7.1) and contain the same original record bytes.
6. For each unit of the shown `ContentBody`, read the ciphertext (5.3) and decrypt it (6.8). Concatenate the plaintexts; their total must equal `size_bytes`. A missing unit means the content is not on this device.
7. Write each file at its path. Reject an export in which a path is both a file and a parent directory. `mtime_epoch_ms` is the modification time and `executable` the owner execute permission.

## 10. Test vectors

`format-vectors/` holds the public golden vectors and `manifest.json`. Each manifest check names the section it exercises, its synthetic inputs (every key, password and seed there is a public test value) and the expected output, from base58 and object names through Argon2id, both unwraps, sealed rows, sealed epoch keys, epoch and index keys, content units under each AEAD, version identities, signed bodies, and complete `CFSSEG02` and `CFSSNP03` containers. `recover.py check-vectors format-vectors` recomputes all of them.

## 11. Format upgrades

When a newer CipherFS opens a vault whose `FORMAT.md` declares an older wire format or database schema in its constants table, it moves the old specification to `format-history/FORMAT-<format>-<schema>.md` and rewrites `FORMAT.md`, `format-vectors/` and `recover.py`. Readers of older data keep the older specification. A vault created without these files is left without them.

## Appendix A: `cipherfs.proto`

```proto
syntax = "proto3";

package cipherfs.v1;

// Persistent field numbers and meanings are never reused; removed fields are reserved.
// Converters reject unspecified, unknown, unsupported, expired or disallowed algorithms.
// Ordinary unknown fields are skipped in projections, while authenticated bodies remain verbatim.
// Map-like repeated entries are sorted by their typed key and reject duplicate keys.
// Other repeated fields retain their existing authored order unless documented otherwise.

// Hash registry; existing numeric assignments are unchanged.
enum HashAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  HASH_ALGORITHM_UNSPECIFIED = 0;
  // BLAKE3, existing ID 1 and text identifier b3.
  HASH_ALGORITHM_BLAKE3 = 1;
  // SHA-256, registered ID 2 and text sha256; current hash execution rejects it.
  HASH_ALGORITHM_SHA256 = 2;
  // SHA-512, registered ID 3 and text sha512; current hash execution rejects it.
  HASH_ALGORITHM_SHA512 = 3;
}

// AEAD registry; existing numeric assignments are unchanged.
enum AeadAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  AEAD_ALGORITHM_UNSPECIFIED = 0;
  // XChaCha20-Poly1305, existing ID 1 and text xchacha20poly1305.
  AEAD_ALGORITHM_XCHACHA20_POLY1305 = 1;
  // AES-256-GCM, existing ID 2 and text aes256gcm.
  AEAD_ALGORITHM_AES256_GCM = 2;
  // ChaCha20-Poly1305, existing ID 3 and text chacha20poly1305.
  AEAD_ALGORITHM_CHACHA20_POLY1305 = 3;
}

// Signing algorithms are independent of public-key algorithms.
enum SignatureAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  SIGNATURE_ALGORITHM_UNSPECIFIED = 0;
  // Ed25519 signing, numeric ID 1 and text ed25519.
  SIGNATURE_ALGORITHM_ED25519 = 1;
}

// Public-key registry includes signing and sealed-box recipient keys.
enum PublicKeyAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  PUBLIC_KEY_ALGORITHM_UNSPECIFIED = 0;
  // Ed25519 signing public key, numeric ID 1 and text ed25519.
  PUBLIC_KEY_ALGORITHM_ED25519 = 1;
  // X25519 sealed-box recipient key, numeric ID 2 and text x25519.
  PUBLIC_KEY_ALGORITHM_X25519 = 2;
}

// Password KDF registry.
enum KdfAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  KDF_ALGORITHM_UNSPECIFIED = 0;
  // Argon2id password derivation, numeric ID 1 and text argon2id.
  KDF_ALGORITHM_ARGON2ID = 1;
}

// Existing writer-side chunking strategies; readers use the stored unit sequence.
enum ChunkingAlgorithm {
  // Invalid unspecified value; converters reject it without fallback.
  CHUNKING_ALGORITHM_UNSPECIFIED = 0;
  // Existing FastCDC 2020 strategy, numeric ID 1 and text fastcdc-2020.
  CHUNKING_ALGORITHM_FAST_CDC_2020 = 1;
  // Existing fixed-size strategy, numeric ID 2 and text fixed.
  CHUNKING_ALGORITHM_FIXED = 2;
}

// Content retention role of a member.
enum Role {
  // Invalid unspecified value; converters reject it without fallback.
  ROLE_UNSPECIFIED = 0;
  // Keeps all unretired content under the existing full-member behavior.
  ROLE_FULL = 1;
  // Keeps pinned and recently read content under the existing partial-member behavior.
  ROLE_PARTIAL = 2;
}

// Existing causes of version retirement.
enum RetireReason {
  // Invalid unspecified value; converters reject it without fallback.
  RETIRE_REASON_UNSPECIFIED = 0;
  // Retirement explicitly requested by the user.
  RETIRE_REASON_EXPLICIT = 1;
  // Retirement selected by the existing retention policy.
  RETIRE_REASON_POLICY = 2;
}

// Typed hash value; algorithm-dependent lengths are checked by converters.
message Digest {
  // Registered hash algorithm identifier.
  HashAlgorithm algorithm = 1;
  // Raw digest bytes of the exact length registered for this hash algorithm.
  bytes value = 2;
}

// Typed signature value; Ed25519 values contain exactly 64 bytes.
message Signature {
  // Registered signature algorithm identifier, independent of public-key algorithms.
  SignatureAlgorithm algorithm = 1;
  // Raw signature bytes of the exact registered length.
  bytes value = 2;
}

// Typed public key; Ed25519 and X25519 values each contain exactly 32 bytes.
message PublicKey {
  // Registered public-key algorithm identifier.
  PublicKeyAlgorithm algorithm = 1;
  // Raw public-key bytes of the exact registered length.
  bytes value = 2;
}

// An identity is derived from these original body bytes with its existing domain label.
message HashedBytes {
  // Original body bytes, retained verbatim rather than decoded and re-encoded.
  bytes body = 1;
}

// Verify the original body before decoding it, then match signer to trusted context.
// Mathematical signature validity alone does not authorize this signer.
message SignedBytes {
  // Original body bytes, retained verbatim rather than decoded and re-encoded.
  bytes body = 1;
  // Signature over body exactly as transmitted.
  Signature signature = 2;
  // Public key used to verify the raw body; trusted authority is checked separately.
  PublicKey signer = 3;
}

// AEAD ciphertext including its authentication tag; AAD remains defined by its context.
message AeadCiphertext {
  // Registered numeric algorithm identifier, checked without fallback.
  AeadAlgorithm algorithm = 1;
  // AEAD nonce of the exact length required by the authenticated algorithm.
  bytes nonce = 2;
  // Encrypted bytes including the authentication tag.
  bytes ciphertext = 3;
}

// Existing asymmetric sealed box; ephemeral_public must identify X25519.
// The trusted parent supplies the recipient key and the existing AAD/domain binding.
message SealedBox {
  // Ephemeral X25519 public key used by this sealed box.
  PublicKey ephemeral_public = 1;
  // Encrypted bytes including the authentication tag.
  AeadCiphertext ciphertext = 2;
}

// Empty table markers and tombstones retain explicit variant presence.
message Empty {}

// Scalar values replace hand-written local integer encodings.
message Uint32Value {
  // Unsigned 32-bit scalar table value.
  uint32 value = 1;
}

// Scalar values replace hand-written local integer encodings.
message Uint64Value {
  // Unsigned 64-bit scalar table value.
  uint64 value = 1;
}

// Local string values, including local directory rows.
message StringValue {
  // Existing UTF-8 string table value.
  string value = 1;
}

// Original signed/encrypted segment objects stored without interpreting their bytes.
message BytesValue {
  // Complete original signed or encrypted object wire bytes.
  bytes value = 1;
}

// Local secret plaintext; callers preserve the table's existing encryption boundary.
message SecretValue {
  // Secret plaintext bytes; callers preserve the existing encryption boundary.
  bytes value_secret = 1;
}

// Password derivation settings; algorithm and parameter variant must agree.
message KdfParameters {
  // Registered numeric algorithm identifier, checked without fallback.
  KdfAlgorithm algorithm = 1;
  // The existing PHC base64 salt string, preserved exactly.
  string salt = 2;
  // Parameters for exactly one known KDF algorithm, matching algorithm.
  oneof parameters {
    // Parameters for the explicitly selected Argon2id algorithm.
    Argon2idParameters argon2id = 3;
  }
}

// Argon2id inputs; no implicit fallback parameters are permitted.
message Argon2idParameters {
  // Exact byte representation of the existing KiB cost; must be divisible by 1024.
  uint64 memory_cost_bytes = 1;
  // Argon2id time cost as the number of iterations.
  uint32 iteration_count = 2;
  // Argon2id parallelism parameter.
  uint32 parallelism = 3;
  // Required derived-key length in bytes.
  uint32 output_length_bytes = 4;
  // Existing Argon2 version 0x13; unsupported versions are rejected.
  uint32 version = 5;
}

// Writer configuration for the existing chunking strategies.
message ChunkingConfiguration {
  // Registered numeric algorithm identifier, checked without fallback.
  ChunkingAlgorithm algorithm = 1;
  // Parameters for exactly one known chunking strategy, matching algorithm.
  oneof parameters {
    // Parameters for the explicitly selected FastCDC 2020 strategy.
    FastCdcParameters fast_cdc = 2;
    // Parameters for the explicitly selected fixed-size strategy.
    FixedChunkingParameters fixed = 3;
  }
}

// FastCDC 2020 writer bounds, retaining current validation.
message FastCdcParameters {
  // Minimum FastCDC chunk size in bytes.
  uint32 min_size_bytes = 1;
  // Target average FastCDC chunk size in bytes.
  uint32 average_size_bytes = 2;
  // Maximum FastCDC chunk size in bytes.
  uint32 max_size_bytes = 3;
}

// Fixed-size writer configuration.
message FixedChunkingParameters {
  // Configured fixed chunk size in bytes.
  uint32 chunk_size_bytes = 1;
}

// Complete original record body; this carrier coexists with any decoded projection.
message IndexRecord {
  // Original body bytes, retained verbatim rather than decoded and re-encoded.
  bytes body = 1;
}

// Decode only after its containing segment has authenticated the original body.
// A missing or unknown variant rejects the complete segment; records are never filtered.
message IndexRecordBody {
  // Exactly one known record variant; missing or unknown variants reject the whole segment.
  oneof kind {
    // Anchored genesis and its immutable initial algorithm policy.
    Genesis genesis = 1;
    // Version identity outside its original hashed preimage.
    VersionEnvelope version = 2;
    // Original member admission, including enrollment capability and proof.
    MemberRecord member = 3;
    // Independent recognition of an exact original recovery admission.
    RecoveryRatification recovery_ratification = 4;
    // Self-authored optional member attribute or capability change.
    MemberUpdate member_update = 5;
    // Publisher's own content, index and store synchronization position.
    SyncPosition sync_position = 6;
    // Existing device cutoff and its newly opened key epoch.
    Revoke revoke = 7;
    // Original new-epoch authority input and sealed key material.
    EpochKeys epoch_keys = 8;
    // Authorized recovery-key rotation.
    RecoveryRotation recovery_rotation = 9;
    // Published vault-wide retention policy.
    RetentionPolicy retention_policy = 10;
    // Retirement identity outside its original hashed preimage.
    RetirementRecord retire = 11;
    // Targets rejected at the retirement grace deadline.
    RetirementOutcome retire_outcome = 12;
    // Existing cancellation of a retirement proposal.
    CancelRetirement cancel_retire = 13;
    // Authored ciphertext-unit ranges within one pack.
    PackLocator pack_locator = 14;
    // Inventory commitment bound to this exact signed author-chain position.
    PackUploadAuthorization pack_upload_authorization = 15;
  }
}

// Anchored genesis; absent recovery keys represent recovery not yet configured.
message Genesis {
  // BLAKE3 commitment binding the initial master key to the anchored genesis.
  Digest master_key_hash = 1;
  // X25519 public key of the recovery recipient.
  PublicKey recovery_pk = 2;
  // Distinct Ed25519 verification key for recovery enrollment approvals.
  PublicKey recovery_verify_pk = 3;
  // Initial algorithm policy taken only after independently authenticating genesis.
  AlgorithmPolicy algorithm_policy = 4;
}

// Immutable initial policy obtained only from authenticated, independently anchored genesis.
// Future policy-transition records are outside this schema's implemented protocol.
message AlgorithmPolicy {
  // Hash algorithm pinned by authenticated genesis.
  HashAlgorithm hash = 1;
  // Signature algorithm pinned by authenticated genesis.
  SignatureAlgorithm signature = 2;
  // Password KDF algorithm pinned by authenticated genesis.
  KdfAlgorithm kdf = 3;
  // Set ordered by numeric AEAD ID, without duplicates.
  repeated AeadAlgorithm aead = 4;
}

// The identity is outside the hashed preimage; data.body is encoded only once.
message VersionEnvelope {
  // BLAKE3 identity of data.body using the existing version domain label.
  Digest id = 1;
  // Original version preimage, preserved without re-encoding.
  HashedBytes data = 2;
}

// Preimage for the existing "cipherfs version id v1" domain label.
message VersionRecordBody {
  // Exact 16-byte random file identity.
  bytes file_id = 1;
  // Superseded heads retain their authored order; duplicate/self parents are rejected.
  repeated Digest parents = 2;
  // Canonical vault-relative path, subject to the existing path validation.
  string path = 3;
  // Exactly one known content-or-deletion variant.
  oneof version {
    // File content references and metadata.
    ContentBody content = 4;
    // Explicit file-deletion variant.
    Empty tombstone = 5;
  }
}

// Content references and metadata retain all existing limits and checks.
message ContentBody {
  // Total file plaintext length in bytes.
  uint64 size_bytes = 1;
  // File modification time as Unix epoch milliseconds.
  uint64 mtime_epoch_ms = 2;
  // Best-effort executable permission.
  bool executable = 3;
  // Exact 16-byte subtree identity; currently the root identity.
  bytes subtree_id = 4;
  // Authenticated content AEAD and hash algorithm pair.
  ContentSuite suite = 5;
  // Exact 32-byte write-lineage identity, independent of any digest algorithm.
  bytes content_id = 6;
  // Units remain in plaintext order.
  repeated UnitRef units = 7;
}

// Existing content suite with explicit registry IDs.
message ContentSuite {
  // Registered AEAD algorithm used for the represented ciphertext.
  AeadAlgorithm aead = 1;
  // Registered content hash identifier; current execution supports only BLAKE3.
  HashAlgorithm hash = 2;
}

// One content unit; nonce validation uses the authenticated enclosing suite.
message UnitRef {
  // BLAKE3 of this unit's complete ciphertext bytes.
  Digest hash = 1;
  // Ciphertext length in bytes, including the AEAD tag.
  uint32 ciphertext_length_bytes = 2;
  // Plaintext length of this content unit in bytes.
  uint32 plaintext_length_bytes = 3;
  // AEAD nonce of the exact length required by the authenticated algorithm.
  bytes nonce = 4;
  // BLAKE3-derived epoch identity, including the all-zero initial epoch.
  Digest epoch = 5;
}

// Original admission; materialized name, role and capabilities never rewrite this body.
message MemberRecord {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Human-readable member name.
  string name = 2;
  // X25519 recipient public key for sealed epoch material.
  PublicKey kx_pk = 3;
  // Member content-retention role.
  Role role = 4;
  // Ed25519 identity that admitted this device, or itself for self-admission.
  PublicKey added_by = 5;
  // Map ordered by typed epoch identity, without duplicates.
  repeated EpochKeyWrap sealed_epoch_keys = 6;
  // Absent for ordinary enrollment; signer must match the trusted recovery verifier.
  SignedBytes recovery_proof = 7;
  // Epoch whose recovery authority authorized the original self-admission.
  Digest enrollment_epoch = 8;
  // Bound into enrollment proofs and required to be at least 1.
  uint32 max_format = 9;
}

// One entry of a member's sealed epoch-key map.
message EpochKeyWrap {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // Epoch-key material sealed to the recipient named by the enclosing record.
  SealedBox sealed_key = 2;
}

// Only the named device may publish its own changes; absence means unchanged.
message MemberUpdate {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Optional replacement member name; absent means unchanged.
  optional string name = 2;
  // Optional replacement content-retention role; absent means unchanged.
  optional Role role = 3;
  // Optional self-advertised format ceiling, at least 1; absent means unchanged.
  optional uint32 max_format = 4;
}

// Publishing device's own position; frontier maps are sorted and reject duplicate keys.
message SyncPosition {
  // Device-keyed map of sequence numbers whose content the publisher holds.
  repeated DeviceSequence content_frontier = 1;
  // Device-keyed map of applied sequence numbers.
  repeated DeviceSequence index_frontier = 2;
  // Store-keyed map of the publisher's per-store device frontiers.
  repeated StoreFrontier stores = 3;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 4;
}

// One entry keyed by the typed device identity.
message DeviceSequence {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Sequence number in the represented device's signed log.
  uint64 seq = 2;
}

// Store entries are keyed by the exact 16-byte ID; each frontier is keyed by device.
message StoreFrontier {
  // Exact 16-byte random store identity.
  bytes store_id = 1;
  // Device-keyed map of sequence numbers published to this store.
  repeated DeviceSequence frontier = 2;
}

// Independent recognition of the exact original recovery admission.
message RecoveryRatification {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Exact original self-Member segment being independently recognized.
  SegmentRef introduction = 2;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 3;
}

// Revocation and its newly opened epoch retain existing causal checks.
message Revoke {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Last sequence of the revoked device that remains accepted.
  uint64 accepted_through_seq = 2;
  // BLAKE3-derived identity of the newly opened key epoch.
  Digest new_epoch = 3;
}

// Original authority input for a new epoch, retaining sealed historical material.
message EpochKeys {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // Causal generation of the key epoch.
  uint64 generation = 2;
  // Historical parent epoch identities in their existing authored order.
  repeated Digest parents = 3;
  // Revoked Ed25519 identities excluded from this epoch.
  repeated PublicKey excluded_devices = 4;
  // X25519 public key of the recovery recipient.
  PublicKey recovery_pk = 5;
  // Distinct Ed25519 verification key for recovery enrollment approvals.
  PublicKey recovery_verify_pk = 6;
  // Map ordered by typed device identity, without duplicates.
  repeated DeviceKeyWrap wraps = 7;
  // Key material sealed to the recovery recipient.
  SealedBox recovery_wrap = 8;
  // Previous epoch key encrypted under the new epoch key.
  AeadCiphertext prev_key_under_new = 9;
}

// One entry of an epoch's per-member sealed-key map.
message DeviceKeyWrap {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Epoch-key material sealed to the recipient named by the enclosing record.
  SealedBox sealed_key = 2;
}

// Existing recovery rotation, authorized under the current authority view.
message RecoveryRotation {
  // X25519 public key of the recovery recipient.
  PublicKey recovery_pk = 1;
  // Distinct Ed25519 verification key for recovery enrollment approvals.
  PublicKey recovery_verify_pk = 2;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 3;
}

// Vault-wide retention policy with the existing day units and defaults.
message RetentionPolicy {
  // Age in days after which non-current versions are considered for retirement.
  uint32 version_max_age_days = 1;
  // Age in days after which deleted files are considered for retirement.
  uint32 trash_max_age_days = 2;
  // Minimum number of versions retained for each file regardless of age.
  uint32 min_versions_per_file = 3;
  // Retirement grace period in days.
  uint32 grace_days = 4;
}

// Retirement identity remains outside the preimage and uses the existing domain label.
message RetirementRecord {
  // BLAKE3 retirement identity of data.body using the existing domain label.
  Digest id = 1;
  // Original retirement preimage, preserved without re-encoding.
  HashedBytes data = 2;
}

// Original retirement preimage binds the segment author and pending timestamp.
message RetirementBody {
  // Ed25519 segment author bound into the retirement preimage.
  PublicKey device = 1;
  // Start of the retirement grace period as Unix epoch milliseconds.
  uint64 pending_since_epoch_ms = 2;
  // Version identities selected by this retirement proposal.
  repeated Digest targets = 3;
  // Existing cause of this retirement proposal.
  RetireReason reason = 4;
}

// Rejected targets retain the existing union behavior after the grace deadline.
message RetirementOutcome {
  // Retirement proposal whose targets were rechecked.
  Digest id = 1;
  // Proposal targets rejected by the grace-deadline recheck.
  repeated Digest rejected_targets = 2;
}

// Existing cancellation of one retirement proposal.
message CancelRetirement {
  // Retirement proposal being cancelled.
  Digest id = 1;
}

// Locations in one pack; pack must have the pack ObjectName variant.
message PackLocator {
  // Typed pack object name; other object variants are rejected.
  ObjectName pack = 1;
  // Unit locations retain their authored order.
  repeated PackUnit units = 2;
}

// One ciphertext unit's bounded range within a pack.
message PackUnit {
  // BLAKE3 of the referenced complete unit ciphertext.
  Digest hash = 1;
  // Byte offset of the unit within its pack.
  uint64 offset_bytes = 2;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 3;
}

// Commitment to an upload inventory at this exact signed author-chain position.
message PackUploadAuthorization {
  // Exact 16-byte random inventory identity.
  bytes inventory_id = 1;
  // BLAKE3 commitment to the inventory body, excluding its external proof.
  Digest inventory_hash = 2;
}

// Parsed object-name projection; textual rendering is canonical algorithm-prefixed base58.
// Unknown textual algorithms are diagnosed and ignored, never used for authority or GC.
message ObjectName {
  // Exactly one known typed object-name variant.
  oneof kind {
    // Vault namespace marker with a random identity.
    VaultObject vault = 1;
    // Store namespace marker with a random identity.
    StoreObject store = 2;
    // Original signed and encrypted per-device segment.
    SegmentObject segment = 3;
    // Pack containing complete ciphertext units.
    PackObject pack = 4;
    // Replay snapshot named by publishing device and complete wire hash.
    SnapshotObject snapshot = 5;
    // Authenticated upload-inventory object.
    UploadInventoryObject upload_inventory = 6;
    // Publisher-signed recovery object named by epoch and complete wire hash.
    RecoveryObject recovery = 7;
  }
}

// Namespace-local random identity rendered as v/base58, with exactly 16 decoded bytes.
message VaultObject {
  // Exact 16-byte random vault identity.
  bytes vault_id = 1;
}

// Namespace-local random identity rendered as s/base58, with exactly 16 decoded bytes.
message StoreObject {
  // Exact 16-byte random store identity.
  bytes store_id = 1;
}

// i/ed25519.base58/seq-b3.base58-length object projection.
message SegmentObject {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Sequence number in the represented device's signed log.
  uint64 seq = 2;
  // BLAKE3 of the complete original segment wire bytes.
  Digest hash = 3;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 4;
}

// p/shard/b3.base58-length object projection; shard uses the first two digest-value characters.
message PackObject {
  // BLAKE3 of the complete pack bytes.
  Digest hash = 1;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 2;
}

// n/ed25519.base58-b3.base58-length projection; frontier remains authenticated in the payload.
message SnapshotObject {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // BLAKE3 of the complete original snapshot wire bytes.
  Digest hash = 2;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 3;
}

// u/b3.base58-length upload-inventory object projection.
message UploadInventoryObject {
  // BLAKE3 of the complete upload-inventory object wire bytes.
  Digest hash = 1;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 2;
}

// r/b3.epoch-base58-b3.hash-base58-length recovery-object projection.
message RecoveryObject {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // BLAKE3 of the complete original recovery-object wire bytes.
  Digest hash = 2;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 3;
}

// Exact causal reference to original complete segment wire bytes.
message SegmentRef {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Sequence number in the represented device's signed log.
  uint64 seq = 2;
  // BLAKE3 of the exact referenced complete segment wire bytes.
  Digest hash = 3;
}

// Existing atomic multi-segment batch identity and 1-based membership.
message BatchPart {
  // Exact 16-byte random batch identity.
  bytes id = 1;
  // One-based part number within this batch.
  uint32 part = 2;
  // Total number of parts in the atomic batch.
  uint32 of = 3;
}

// Encrypted segment plaintext; authentication uses the original fixed-header/wire bytes.
message SegmentPayload {
  // Exact 16-byte random vault identity.
  bytes vault_id = 1;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 2;
  // Sequence number in the represented device's signed log.
  uint64 seq = 3;
  // Absent only for the author's first segment.
  Digest prev = 4;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 5;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 6;
  // Original causal references to authenticated segments of other devices.
  repeated SegmentRef deps = 7;
  // Presence distinguishes a single segment from a batch part.
  BatchPart batch = 8;
  // Original record carriers retain segment order and unknown body fields.
  repeated IndexRecord records = 9;
}

// Semantic projection of the fixed CFSSEG02 header; this is not its framing encoding.
// Original magic, big-endian format, algorithm IDs and nonce are all signed and AEAD AAD.
message SegmentHeader {
  // Explicit wire-format number; unsupported values are rejected before application.
  uint32 format = 1;
  // Claimed Ed25519 author; parsing the header alone grants no authority.
  PublicKey device = 2;
  // Sequence number in the represented device's signed log.
  uint64 seq = 3;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 4;
  // Registered AEAD algorithm used for the represented ciphertext.
  AeadAlgorithm aead = 5;
  // Registered outer-signature algorithm authenticated with the original header.
  SignatureAlgorithm signature = 6;
  // AEAD nonce of the exact length required by the authenticated algorithm.
  bytes nonce = 7;
}

// Semantic projection of the fixed CFSSNP03 header; this is not its framing encoding.
// Original header and authority prefix bytes are signed and AEAD AAD before decryption.
message SnapshotHeader {
  // Explicit wire-format number; unsupported values are rejected before application.
  uint32 format = 1;
  // Claimed Ed25519 publisher; parsing the header alone grants no authority.
  PublicKey device = 2;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 3;
  // Sum of the covered frontier sequence numbers.
  uint64 total_seqs = 4;
  // Total original wire bytes of the covered accepted segments.
  uint64 total_bytes = 5;
  // Registered AEAD algorithm used for the represented ciphertext.
  AeadAlgorithm aead = 6;
  // Registered outer-signature algorithm authenticated with the original header.
  SignatureAlgorithm signature = 7;
  // AEAD nonce of the exact length required by the authenticated algorithm.
  bytes nonce = 8;
}

// Original signed and encrypted authority segments, retaining proof order and exact bytes.
message AuthorityProof {
  // Original complete signed and encrypted segment bytes in proof order.
  repeated bytes segments = 1;
}

// Snapshot frontier commitment preimage, ordered by device and encoded only once.
message FrontierBody {
  // Device-keyed snapshot frontier map, ordered without duplicate keys.
  repeated DeviceFrontierEntry entries = 1;
}

// One device-keyed entry of the snapshot frontier map.
message DeviceFrontierEntry {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Accepted and applied frontier metadata for this exact device.
  FrontierEntry entry = 2;
}

// Snapshot replay inputs; derived views are rebuilt rather than trusted independently.
message SnapshotPayload {
  // Exact 16-byte random vault identity.
  bytes vault_id = 1;
  // Original authority proof is carried by the authenticated prefix in CFSSNP03.
  AuthorityProof authority_segments = 2;
  // Preserve this body verbatim; changing object names does not remove frontier binding.
  HashedBytes frontier = 3;
  // BLAKE3 identity of frontier.body using the existing frontier domain label.
  Digest frontier_hash = 4;
  // Map ordered by (device, seq), without duplicate keys.
  repeated SegmentMetadataEntry segment_metas = 5;
  // Complete covered version replay inputs, including original record carriers.
  repeated StoredVersion versions = 6;
  // Complete covered non-version replay inputs with original record carriers.
  repeated IndexEvent index_events = 7;
}

// One entry of the replay segment-metadata map.
message SegmentMetadataEntry {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Sequence number in the represented device's signed log.
  uint64 seq = 2;
  // Accepted metadata of the exact device and sequence map entry.
  SegmentMeta meta = 3;
}

// Highest accepted and applied sequence numbers of a device.
message FrontierEntry {
  // Highest accepted segment sequence of this device.
  uint64 seq = 1;
  // BLAKE3 of the complete accepted segment at seq.
  Digest hash = 2;
  // Highest sequence whose records have been applied, including complete batches only.
  uint64 applied_seq = 3;
  // Total original wire bytes of the covered accepted segments.
  uint64 total_bytes = 4;
}

// Metadata of an accepted segment, shared by the local database and snapshot replay.
message SegmentMeta {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // Previous complete segment hash; absent only at the first sequence.
  Digest prev = 2;
  // BLAKE3 of the complete accepted segment wire bytes.
  Digest hash = 3;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 4;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 5;
  // Original causal references to authenticated segments of other devices.
  repeated SegmentRef deps = 6;
  // Optional membership in an atomic multi-segment batch.
  BatchPart batch = 7;
  // Required supported format; snapshots must not bypass an author's format gate.
  uint32 format = 8;
}

// Durable barrier from an authenticated newer-format segment of an existing member.
message FormatBlock {
  // Ed25519 author whose ordinary import is blocked at this position.
  PublicKey device = 1;
  // First authenticated unsupported sequence; later segments must not advance its frontier.
  uint64 blocked_seq = 2;
  // Authenticated format above the local executable's actual supported maximum.
  uint32 format = 3;
  // BLAKE3 of the complete original signed segment establishing this barrier.
  Digest segment_hash = 4;
}

// Original non-version replay input and its authenticated segment position.
message IndexEvent {
  // Complete original non-version IndexRecord body retained for replay.
  IndexRecord record = 1;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 2;
  // Sequence number in the represented device's signed log.
  uint64 seq = 3;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 4;
  // Zero-based record position within the authenticated segment.
  uint32 ordinal = 5;
}

// Original authority input remains available after revocation and across local reopen.
message AuthorityEvent {
  // Complete original authority IndexRecord body retained after revocation.
  IndexRecord record = 1;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 2;
  // Sequence number in the represented device's signed log.
  uint64 seq = 3;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 4;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 5;
  // BLAKE3 of the original carrying segment wire bytes.
  Digest hash = 6;
  // Original causal references to authenticated segments of other devices.
  repeated SegmentRef deps = 7;
  // Zero-based record position within the authenticated segment.
  uint32 ordinal = 8;
}

// A version projection and its containing authenticated record must coexist.
message StoredVersion {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // Version envelope projection retained alongside the complete original record.
  VersionEnvelope version = 2;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 3;
  // Sequence number in the represented device's signed log.
  uint64 seq = 4;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 5;
  // Retains unknown fields in IndexRecordBody outside the nested version body as well.
  IndexRecord original_record = 6;
}

// Immutable admission carrier plus the member's derived current mutable attributes.
message MemberState {
  // Complete immutable original Member record body, independent of mutable attributes.
  IndexRecord admission = 1;
  // Current derived name, independent of the immutable original admission body.
  string name = 2;
  // Current derived role, independent of the immutable original admission body.
  Role role = 3;
  // Current self-advertised format ceiling, independent of the original admission body.
  uint32 max_format = 4;
}

// Independently saved public anchor, never obtained solely from an untrusted object listing.
message TrustAnchor {
  // Exact 16-byte random vault identity.
  bytes vault_id = 1;
  // Ed25519 signer of the independently anchored genesis segment.
  PublicKey genesis_device = 2;
  // BLAKE3 of the complete authenticated genesis segment wire bytes.
  Digest genesis_hash = 3;
}

// Sealed device-local identity, retaining the current table's confidentiality boundary.
message LocalIdentity {
  // Exact 16-byte random vault identity.
  bytes vault_id = 1;
  // Independently saved vault genesis trust anchor.
  TrustAnchor trust_anchor = 2;
  // Exactly 32 secret bytes; never included in logs or public fixtures.
  bytes device_seed_secret = 3;
  // Human-readable name of this local device.
  string device_name = 4;
}

// Plaintext wrapped by both password and recovery configuration, distinct from LocalIdentity.
message LocalSecretsBody {
  // Exactly 32 secret bytes.
  bytes device_seed_secret = 1;
  // Exactly 32 secret bytes.
  bytes local_storage_key_secret = 2;
}

// One local epoch key or an entry inside an asymmetrically sealed epoch-key bundle.
message EpochKeyMaterial {
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 1;
  // Exactly 32 secret bytes, preserving the existing sealed/storage boundary.
  bytes key_secret = 2;
}

// Epoch-key map ordered by typed epoch identity, without duplicate keys.
message EpochKeyBundle {
  // Epoch-key map ordered by typed epoch identity, without duplicate keys.
  repeated EpochKeyMaterial keys = 1;
}

// Configuration itself is unsealed; each secret wrap retains its own encryption.
message VaultConfig {
  // Device seed and local storage key encrypted by the password-derived KEK.
  AeadCiphertext password_wrap = 1;
  // Explicit password derivation algorithm and parameters.
  KdfParameters kdf = 2;
  // Key material sealed to the recovery recipient.
  SealedBox recovery_wrap = 3;
  // X25519 public key of the recovery recipient.
  PublicKey recovery_pk = 4;
}

// Materialized local file state.
message FileState {
  // Current causal head version identities for this file.
  repeated Digest heads = 1;
  // Selected current version identity.
  Digest current = 2;
  // Selected current version's canonical vault path.
  string path = 3;
  // Whether the selected current version is a tombstone.
  bool deleted = 4;
  // Selected current version's publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 5;
}

// Materialized files at a path.
message PathEntry {
  // Canonical vault-relative path, subject to the existing path validation.
  string path = 1;
  // Each file identity contains exactly 16 bytes.
  repeated bytes files = 2;
}

// Derived child counts of a directory.
message ChildEntry {
  // Immediate child path component within its parent directory.
  string name = 1;
  // Number of live file paths equal to this immediate child path.
  uint32 file_paths = 2;
  // Number of live paths and local directories beneath this child.
  uint64 below = 3;
}

// Deleted-file row in the local trash view.
message TrashRow {
  // Canonical vault-relative path, subject to the existing path validation.
  string path = 1;
  // Deletion publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 2;
}

// Local synchronization-position table retains its originating author sequence.
message StoredSyncPosition {
  // Sequence number in the represented device's signed log.
  uint64 seq = 1;
  // Original synchronization position published at the stored sequence.
  SyncPosition position = 2;
}

// Published policy and its existing last-writer rank.
message PolicyPublication {
  // Published vault-wide retention policy.
  RetentionPolicy policy = 1;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 2;
  // Sequence number in the represented device's signed log.
  uint64 seq = 3;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 4;
}

// Materialized retirement proposal, separate from its original authenticated preimage.
message RetirementStateRow {
  // Identity of the materialized retirement proposal.
  Digest id = 1;
  // Version identities selected by this retirement proposal.
  repeated Digest targets = 2;
  // Existing cause of this retirement proposal.
  RetireReason reason = 3;
  // Start of the retirement grace period as Unix epoch milliseconds.
  uint64 pending_since_epoch_ms = 4;
  // Retirement grace period in days.
  uint32 grace_days = 5;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 6;
  // Sequence number in the represented device's signed log.
  uint64 seq = 7;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 8;
}

// Materialized retirement cancellation rank.
message Cancellation {
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 1;
  // Sequence number in the represented device's signed log.
  uint64 seq = 2;
  // Original publication time as Unix epoch milliseconds.
  uint64 time_epoch_ms = 3;
}

// All original materialized cancellations for a proposal.
message CancellationList {
  // Original materialized cancellation ranks in existing list order.
  repeated Cancellation entries = 1;
}

// Existing retirement-effect timestamps, preserving zero timestamp semantics.
message RetiredVersion {
  // Grace deadline as Unix epoch milliseconds.
  uint64 effect_epoch_ms = 1;
  // Earliest local content-drop time as Unix epoch milliseconds.
  uint64 local_drop_epoch_ms = 2;
  // Earliest remote-store reclamation time as Unix epoch milliseconds.
  uint64 store_gc_epoch_ms = 3;
}

// Local version lists, including segment-version and rejected-target table values.
message VersionIdList {
  // Version identities in the existing table value's list or set order.
  repeated Digest versions = 1;
}

// Stored remote-GC observation; optional repacking time retains presence.
message StoreGcObservation {
  // First local observation time as Unix epoch milliseconds.
  uint64 first_seen_epoch_ms = 1;
  // Optional repacking completion time as Unix epoch milliseconds.
  optional uint64 repacked_epoch_ms = 2;
  // Replacement pack object names observed during repacking.
  repeated ObjectName replacements = 3;
}

// A remote unit location projection.
message UnitLocation {
  // Typed pack object name; other object variants are rejected.
  ObjectName pack = 1;
  // Byte offset of the unit within its pack.
  uint64 offset_bytes = 2;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 3;
}

// Local pack-location table value; pack identity remains in the existing table key.
message PackLocatorValue {
  // Total pack length in bytes, retained with this local location.
  uint64 pack_length_bytes = 1;
  // Byte offset of the unit within its pack.
  uint64 offset_bytes = 2;
  // Length in bytes of the object or range represented by this message.
  uint64 length_bytes = 3;
}

// Local policy remains device-local and keeps its existing validation.
message LocalContentPolicy {
  // Device-local directory rules in their existing configured order.
  repeated string pinned_directories = 1;
  // Maximum device-local read-cache allocation in bytes.
  uint64 read_cache_max_bytes = 2;
}

// Opaque peer routes retain configuration order and the existing Core API representation.
message SyncPeers {
  // Device-local opaque peer routes in configured order.
  repeated string routes = 1;
}

// One device-local store configuration; credentials remain external value sources.
message StoreConfig {
  // Exact 16-byte random store identity.
  bytes store_id = 1;
  // Existing directory or S3-compatible location variant.
  StoreLocation location = 2;
}

// Existing location variants only.
message StoreLocation {
  // Exactly one known existing store-location variant.
  oneof kind {
    // Existing local or cloud-synced folder location.
    DirectoryLocation directory = 1;
    // Existing S3-compatible bucket location.
    S3Location s3 = 2;
  }
}

// Existing UTF-8 directory-path representation.
message DirectoryLocation {
  // Existing UTF-8 absolute local or cloud-synced directory path.
  string path = 1;
}

// Existing S3-compatible configuration; absent endpoint/prefix differs from an empty value.
message S3Location {
  // S3-compatible bucket name.
  string bucket = 1;
  // S3-compatible region name.
  string region = 2;
  // Optional S3-compatible endpoint URL; absent differs from an empty value.
  optional string endpoint_url = 3;
  // Optional object-key prefix; absent differs from an empty value.
  optional string prefix = 4;
}

// Store configurations preserve input order; duplicate store IDs are rejected.
message StoreConfigList {
  // Store configurations in input order; duplicate store identities are rejected.
  repeated StoreConfig stores = 1;
}

// Original recovery envelope cached with the commitment to the current authority inputs.
message RecoveryEnvelopeCache {
  // BLAKE3 commitment to the original current authority inputs.
  Digest authority_hash = 1;
  // Complete original signed recovery-envelope bytes.
  bytes envelope = 2;
}

// Explicit database schema setting; absent is corruption for an existing database.
message DatabaseSchema {
  // Present database schema number; missing in an existing database is corruption.
  optional uint32 schema = 1;
}

// CAS reference-count table value.
message CasReferenceCount {
  // Existing unsigned CAS reference count.
  uint32 count = 1;
}

// CAS access-time table value.
message CasAccessTime {
  // Last CAS access time as Unix epoch milliseconds.
  uint64 accessed_epoch_ms = 1;
}

// CAS packed-chunk table value, retaining its existing integer widths.
message CasPackedLocation {
  // Existing unsigned local CAS pack identifier.
  uint32 pack_id = 1;
  // Byte offset of the unit within its pack.
  uint64 offset_bytes = 2;
  // Length in bytes of the object or range represented by this message.
  uint32 length_bytes = 3;
}

// Public enrollment bootstrap data; store configurations retain input order.
message InitInfo {
  // Independently saved vault genesis trust anchor.
  TrustAnchor trust_anchor = 1;
  // Configured stores in the original enrollment input order.
  repeated StoreConfig stores = 2;
}

// Signed enrollment request carrier, with its original authenticated body.
message EnrollmentRequest {
  // Original requester-signed enrollment carrier.
  SignedBytes request = 1;
}

// Original requester-signed preimage; signer must equal device after authentication.
message EnrollmentRequestBody {
  // Existing "cipherfs device enrollment v1" domain.
  string domain = 1;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 2;
  // X25519 recipient public key for sealed epoch material.
  PublicKey kx_pk = 3;
  // Human-readable member name.
  string name = 4;
  // Member content-retention role.
  Role role = 5;
  // Maximum supported wire format, required to be at least 1 when present.
  uint32 max_format = 6;
}

// Original recovery-approved admission preimage, bound to its vault, epoch and capabilities.
message RecoveryEnrollmentBody {
  // Existing "cipherfs recovery enrollment v1" domain.
  string domain = 1;
  // Exact 16-byte random vault identity.
  bytes vault_id = 2;
  // Ed25519 identity of the device represented by this message.
  PublicKey device = 3;
  // X25519 recipient public key for sealed epoch material.
  PublicKey kx_pk = 4;
  // Human-readable member name.
  string name = 5;
  // Member content-retention role.
  Role role = 6;
  // Epoch whose recovery authority authorized the original self-admission.
  Digest enrollment_epoch = 7;
  // Maximum supported wire format, required to be at least 1 when present.
  uint32 max_format = 8;
}

// Only sealed keys and original authenticated admission/proof bytes cross enrollment.
message EnrollmentBundle {
  // Public genesis anchor and configured stores for enrollment bootstrap.
  InitInfo init_info = 1;
  // Complete original Member IndexRecord body, rather than a re-encoded projection.
  IndexRecord member = 2;
  // Original signed authority proof checked against the independent genesis anchor.
  AuthorityProof authority_segments = 3;
}

// Original publisher-signed recovery object; no snapshot framing is implied.
message RecoveryEnvelope {
  // Original publisher-signed recovery envelope carrier.
  SignedBytes data = 1;
}

// Recovery envelope preimage retains its original authority and recipient bindings.
message RecoveryEnvelopeBody {
  // Existing "cipherfs recovery envelope v1" domain.
  string domain = 1;
  // Independently checked genesis trust anchor bound by this recovery object.
  TrustAnchor anchor = 2;
  // Ed25519 identity of the publishing device.
  PublicKey publisher = 3;
  // BLAKE3-derived key-epoch identity; the initial epoch has an all-zero value.
  Digest epoch = 4;
  // Causal generation of the key epoch.
  uint64 generation = 5;
  // X25519 public key of the recovery recipient.
  PublicKey recovery_pk = 6;
  // Epoch-key bundle sealed to the specified recovery recipient.
  SealedBox sealed_keys = 7;
  // Original signed authority proof included in the publisher-signed body.
  AuthorityProof authority_segments = 8;
}

// Local recovery-cache authority commitment preimage.
message RecoveryAuthorityBody {
  // Current key-epoch identity included in the authority commitment.
  Digest current_epoch = 1;
  // Original authority events in their existing commitment order.
  repeated AuthorityEvent events = 2;
}

// Inventory commitment preimage deliberately excludes the external authorization proof.
message InventoryCommittedBody {
  // Exact 16-byte random immutable upload-inventory identity.
  bytes inventory_id = 1;
  // Complete pack locator whose units are committed by this inventory.
  PackLocator locator = 2;
  // Observed index frontier before this upload began.
  repeated SegmentRef frontier = 3;
  // Upload-start publication time as Unix epoch milliseconds.
  uint64 published_epoch_ms = 4;
  // Ed25519 identity of the publishing device.
  PublicKey publisher = 5;
  // Exact author-chain position carrying the inventory authorization.
  uint64 publisher_seq = 6;
  // Exact signed segment object names carrying the referenced units.
  repeated ObjectName referencing_segments = 7;
}

// Original committed inventory bytes plus independently checked original authorization proof.
message PackUploadInventory {
  // Original inventory commitment body, retained verbatim.
  HashedBytes committed = 1;
  // BLAKE3 identity derived from the original committed inventory body.
  Digest commitment = 2;
  // External original signed chains proving the exact inventory authorization.
  AuthorityProof authorization_segments = 3;
}

// Explicit endpoint address replaces dependency-owned positional serialization.
message NodeAddress {
  // Ed25519 identity of the authenticated endpoint.
  PublicKey node_id = 1;
  // Optional endpoint relay URL, preserving absence.
  optional string relay_url = 2;
  // Address set sorted by the existing socket-address ordering, without duplicates.
  repeated SocketAddress direct_addresses = 3;
}

// IPv4 uses 4 address bytes and IPv6 uses 16; port must fit u16.
// IPv6 flow/scope presence and values are preserved; IPv4 must omit both fields.
message SocketAddress {
  // Four IPv4 bytes or sixteen IPv6 bytes in network order.
  bytes ip_address = 1;
  // Socket port, checked to fit the existing u16 representation.
  uint32 port = 2;
  // IPv6 flow information; IPv4 addresses must omit this field.
  optional uint32 flow_info = 3;
  // IPv6 scope identifier; IPv4 addresses must omit this field.
  optional uint32 scope_id = 4;
}

// One-time pairing ticket; the secret is only disclosed through the existing explicit UI.
message PairTicket {
  // Explicit address of the joining device's existing pairing endpoint.
  NodeAddress node_addr = 1;
  // Exactly 16 secret bytes; never logged or included in public fixtures.
  bytes pairing_secret = 2;
}

// Raw challenge-MAC input binds both authenticated endpoint identities and a 32-byte nonce.
message PairChallengeBody {
  // Ed25519 identity of the existing-vault endpoint.
  PublicKey existing = 1;
  // Ed25519 identity of the joining endpoint.
  PublicKey joining = 2;
  // Original 32-byte challenge nonce bound into the existing keyed MAC.
  bytes nonce = 3;
}

// Raw pairing transcript is encoded once and shared by the existing MAC and SAS operations.
message PairTranscriptBody {
  // Existing "cipherfs pair transcript v1" domain.
  string domain = 1;
  // Ed25519 identity of the existing-vault endpoint.
  PublicKey existing = 2;
  // Ed25519 identity of the joining endpoint.
  PublicKey joining = 3;
  // Original 32-byte challenge nonce from the existing device.
  bytes first_nonce = 4;
  // Original 32-byte response nonce from the joining device.
  bytes second_nonce = 5;
  // Original signed enrollment carrier embedded in the raw transcript.
  EnrollmentRequest enrollment_request = 6;
}

// Existing ticket-secret keyed BLAKE3 proof; it is a MAC, not a signature.
message PairChallenge {
  // Original body bytes, retained verbatim rather than decoded and re-encoded.
  HashedBytes body = 1;
  // Existing ticket-secret keyed BLAKE3 MAC, checked against the original body or transcript.
  Digest proof = 2;
}

// Request and second nonce are decoded from the original authenticated transcript.
message PairResponse {
  // Original transcript bytes authenticated by the existing ticket-secret MAC.
  HashedBytes transcript = 1;
  // Existing ticket-secret keyed BLAKE3 MAC, checked against the original body or transcript.
  Digest proof = 2;
}

// Existing approval MAC binds the already shared raw transcript.
message PairApproval {
  // Existing ticket-secret keyed BLAKE3 MAC, checked against the original body or transcript.
  Digest proof = 1;
  // Authenticated enrollment result sent after the existing explicit approval.
  EnrollmentBundle bundle = 2;
}

// Existing joining-device acknowledgement after its local approval succeeds.
message PairAck {
  // Existing joining-device acknowledgement after its own approval succeeds.
  bool accepted = 1;
}

// Existing pairing phases only; no independent transcript-signature feature is added.
message PairFrame {
  // Exactly one known existing pairing phase.
  oneof kind {
    // Existing-device ticket-secret challenge phase.
    PairChallenge challenge = 1;
    // Joining-device ticket-secret response and original enrollment transcript.
    PairResponse response = 2;
    // Existing-device explicit approval and authenticated enrollment bundle.
    PairApproval approval = 3;
    // Existing joining-device boolean acknowledgement phase.
    PairAck acknowledgement = 4;
  }
}
```
