> Mirrored from https://github.com/RelicSync/relic/blob/main/docs/crypto.md on every deploy of relic.space. The index of developer docs is https://relic.space/developers.

# Relic — Crypto Specification v1

Pinned parameters. Every client must implement exactly this; the format version
gates any future change. Nothing here is negotiable at runtime.

**Implementations (all wire-identical, byte-verified against each other with
the pinned vectors in `crypto/test/relic_crypto_test.dart`):**

1. **Dart** — `crypto/lib/relic_crypto.dart`, the Apache-2.0 package the
   desktop and mobile apps depend on (the reference implementation in
   practice).
2. **JS/TS** — `crypto/js/vault-crypto.ts` (+ `vault-kek.worker.ts` for
   off-thread Argon2) — the web vault at relic.space/vault. Uses `hash-wasm`
   (Argon2id) and `@noble/ciphers` (XChaCha20-Poly1305).
3. **Rust** — `relic-core` (historical origin of the format; the crate remains
   the Rust reference).

Change anything here and you must change all of them, plus this doc, together.

## Key hierarchy (wrapped master key)

```
passphrase ──Argon2id──▶ KEK (32B)  ──unwraps──▶ MK (32B, random)  ──encrypts──▶ relics & blobs
recovery kit ───────────────────────── is ─────▶ MK (verbatim)
```

- **MK (master key):** 32 random bytes (`OsRng`), generated once on the first
  device at key setup. Encrypts all content, forever.
- **KEK (key-encryption key):** derived from the user's encryption passphrase.
  Wraps MK. Changing the passphrase re-wraps MK — **no data re-encryption**.
- **Recovery kit:** the raw MK, base32 (Crockford) in groups of 4, plus account
  email and `format: relic-mk-v1`. Printable/downloadable at setup; never
  stored by the operator. Holding the kit allows re-wrapping under a new
  passphrase (full recovery without the old passphrase).

Rationale vs. deriving the data key directly from the passphrase (spec v0.4 and
earlier): passphrase changes become cheap, and the recovery kit is a stable
artifact that survives passphrase rotation.

## KDF — Argon2id

| param | value |
|---|---|
| variant / version | Argon2id, v1.3 (0x13) |
| memory | 64 MiB |
| iterations | 3 |
| parallelism | 4 |
| salt | 16 random bytes, per account |
| output | 32 bytes (KEK) |

The salt is generated by the first device and stored (plaintext — salts are not
secret) in the account's key-params record so every device derives the same KEK.

**Test vector** (every client implementation must reproduce this):
`Argon2id("correct horse battery staple", salt = 0x42×16, pinned params)` =
`dde64ab2660a0dedd449fa0414587bd0407e68ebe4a3f1595f2c167f1f7ff3cd`

## AEAD — XChaCha20-Poly1305

24-byte random nonce per encryption (`OsRng`); collision risk negligible at any
realistic volume. The nonce is stored alongside each ciphertext. Domain
separation via AAD:

| use | AAD |
|---|---|
| MK wrap | `relic.mkwrap.v1` |
| relic payload | `relic.relic.v1:<uid>` |
| blob | `relic.blob.v1:<blob_id>` |

Binding `uid`/`blob_id` into the AAD prevents ciphertext swap/mix-and-match by
a malicious or compromised server. The blob id is **client-generated** and IS
the wire-protocol `blob_key` (bare); servers map it into their own namespace
(`users/<acct>/blob/<id>` on R2) internally.

## Key-params record (server-side, per account)

```json
{
  "v": 1,
  "salt": "<b64 16B>",
  "argon2": { "m_kib": 65536, "t": 3, "p": 4 },
  "mk_nonce": "<b64 24B>",
  "wrapped_mk": "<b64 48B>"        // AEAD(KEK, mk_nonce, MK, aad=mkwrap)
}
```

Stored via `PUT /keyparams` by the first device; fetched by every new device.
Safe to store server-side: recovering MK from it requires the passphrase.
**Unwrap failure = wrong passphrase** — this doubles as the key-check, so
clients never need to trial-decrypt a real relic.

## Handling rules

- MK and KEK live only in memory, in `zeroize`-d buffers; MK may additionally
  be cached in OS secure storage (Keychain/Keystore/Credential Manager) behind
  the unlock gate (SPEC §12).
- Plaintext relic payloads are decrypted on demand and zeroized after use.
- Passphrase input buffers are zeroized after KDF.
- Blobs (≤100 MB) are encrypted as a single AEAD message in v1. A chunked
  streaming format is a future format-version bump (needed before any cap
  raise; chunking would use a per-blob subkey and counter nonces).
