# Protocol v1

## Encryption

Recovery key: 32 CSPRNG bytes, encoded as `mv1_` + unpadded base64url. Derive two 32-byte keys with HKDF-SHA256, salt UTF-8 `meshvault-v1`:

- Ed25519 seed info: `meshvault/signing/v1`
- AES-256 key info: `meshvault/encryption/v1`

Owner = lowercase hex SHA-256 of the 32-byte public key. Go uses standard-library crypto; Apple uses CryptoKit. Public synthetic interop fixtures verify both directions.

Chunks are at most 4 MiB. Each is AES-256-GCM encrypted with a fresh random 12-byte nonce. Stored format: nonce || ciphertext || 16-byte tag. AAD: UTF-8 `meshvault-v1:<owner>:chunk`. Chunk ID = lowercase hex SHA-256 of the stored bytes. There is no plaintext deduplication across users.

The JSON manifest uses the same encryption with AAD `meshvault-v1:<owner>:manifest`. Its fields include source identity/name, relative path, writer, kind, timestamps, plaintext size/hash, ordered chunk IDs, and a deletion flag. Only ciphertext leaves the owner device.

## Signed records

Envelope JSON fields: `owner`, `public_key`, `body`, `blobs`, `signature`. Public key, encrypted body, and signature use standard padded base64. `blobs` repeats ordered encrypted chunk IDs for complete-copy checks without decryption.

Ed25519 signs this exact UTF-8 string, without a trailing newline:

```text
meshvault-record-v1
<owner>
<public_key>
<body>
<comma-separated blobs>
```

Record ID = SHA-256(message || newline || base64 signature), in lowercase hex. Clients verify ownership, signature, AES-GCM, manifest version, repeated chunk references, restored size, and whole-file digest. Versions are immutable. Deletions add encrypted tombstones and leave earlier versions intact.

## Requests

Every request has `Authorization: Bearer <network-admission-key>`. Go clients only dial Tailscale IPv4/IPv6 ranges, recheck DNS results at connect time, ignore proxy environment variables, and reject redirects. Test mode also permits loopback. Apple clients validate Tailscale URL forms and reject redirects; MagicDNS resolution relies on the phone's configured Tailscale network.

User endpoints also require `X-Owner`, `X-Public-Key`, `X-Time` (Unix seconds), `X-Nonce` (16 random bytes in lowercase hex), and `X-Signature`. Ed25519 signs the following UTF-8 bytes, with no final newline:

```text
meshvault-request-v1
<HTTP method>
<percent-encoded URL path, excluding query>
<X-Time>
<X-Nonce>
<lowercase hex SHA-256 of exact body>
```

Owner must match the public-key hash. Clock skew is limited to five minutes; a bounded in-memory cache rejects repeated nonces. Restarts forget that cache, so writes remain immutable/idempotent and there is no destructive endpoint. Retries use new signatures/nonces.

| Endpoint | Required capability | Behavior |
| --- | --- | --- |
| `GET /v1/health` | Network key | Node ID, usage, target copies |
| `GET/PUT /v1/owners/{owner}/blobs/{id}` | Network + user signature | Integrity-checked encrypted chunk |
| `PUT /v1/owners/{owner}/records/{id}` | Network + user signature | Verify record and all chunks before commit |
| `GET /v1/owners/{owner}/records` | Network + user signature | Valid records with complete local chunks |
| `GET /v1/owners/{owner}/records/{id}` | Network + user signature | Signed encrypted record |
| `GET /v1/node/inventory` | Network key | Integrity-checked opaque inventory |
| `GET/PUT /v1/node/{kind}/{owner}/{id}` | Network key | Replicate `blobs` or `records`; records still need valid owner signatures |

Network members can relay any ciphertext namespace; they cannot generate user signatures or decrypt other users. The network admission key represents trusted storage membership. A malicious member can consume quota or deny service. Revoking it requires network-key rotation and tailnet access changes, which do not rotate the user key.

## Storage and repair

Private directories/files use 0700/0600. Atomic writes use a temporary file, file fsync, rename, and directory fsync. A process lock prevents two daemons from writing the same data directory. Quota checks and commits share a mutex. Invalid hashes/signatures are not advertised as healthy and can be repaired.

Every 30 seconds, repair takes the union of peer inventories, pulling missing/corrupted objects and pushing objects absent on peers. Chunks transfer before records. Every node retains all ciphertext: the replica goal never authorizes pruning. A full mesh makes health counts and repair straightforward; graph topologies also converge.

Copies count distinct, configured peer identities whose recent inventory contains the record and every chunk, plus intact local storage. An offline observation immediately removes that peer from the count. Inventory observations expire after two minutes. This is asynchronous replication, not a quorum write guarantee.

Restores can fetch intact chunks from surviving peers, check hashes and AES-GCM, and verify the final plaintext digest. Go `os.Root` confines file operations to the chosen destination even through symlinks. Atomic hard-link creation refuses overwrites. There is no garbage collector or destructive reconciliation.

## Local client

The management API uses a random loopback port and per-process bearer token, rejects Origin headers, and never runs on the peer listener. It handles user unlock, folder watching, catalog/history, restore, network settings and repair. User private keys exist in memory only; key export in the Mac app reads its Keychain.

Local private configuration contains the network key and source folder paths. Keychain secrets are device-local. iPhone checkpoints are encrypted with kind `photo-state`. Original Photos resources briefly use the phone's protected temporary directory, then are encrypted and removed; stale staging is removed on the next backup.

## References

- [Go HKDF](https://pkg.go.dev/crypto/hkdf)
- [Apple CryptoKit](https://developer.apple.com/documentation/cryptokit)
- [Apple background processing](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtaskrequest)
- [Tailscale address ranges](https://tailscale.com/docs/reference/reserved-ip-addresses)
