// docs
keys & collaboration keys
Every cryptographic decision in CozyLabs is fixed in one small package (@collab/crypto, a thin
wrapper over libsodium): algorithms, key lengths, and nonce policy are not configurable, so they
cannot be misconfigured.
identities
A principal — human or agent — owns an identity of two keypairs:
- X25519 for encryption (sealed boxes; receiving wrapped keys)
- Ed25519 for signing (event signatures, management proofs, key manifests)
The relay stores only the public halves. A deterministic keyId (BLAKE2b hash of the
canonical public-key pair) names a specific key generation, and a self-signed
PrincipalKeyManifest binds principalId ↔ keys ↔ keyId so anyone can verify a principal’s
key material without asking the relay.
For humans-out-of-band comparison there’s a short key fingerprint rendered the same way in
the CLI and the dashboard, so two people on a call can compare character-for-character:
ed25519:a4f2·9c01·77be·d3e8.
the collaboration key
One collaboration, one CK: a random 32-byte XChaCha20-Poly1305 key that encrypts all content in that room — events via AEAD (with the routing metadata as associated data, so ciphertext can’t be replayed under different routing), artifacts via a chunked secretstream that detects truncation.
The CK itself is never stored or sent in the clear. It moves only by wrapping:
| path | mechanism |
|---|---|
| member | wrappedCk — CK sealed to the member’s X25519 public key, stored on their membership |
| invite | cks — CK wrapped under a key derived from a high-entropy invite secret S; S travels in the link’s URL fragment and is never stored server-side |
| agent approval | the approving human wraps CK to the agent’s public key at approval time |
The relay can hand wrappedCk to exactly one party who can use it: you. GET /collabs/:id/me
returns your own wrapped copy; the general members list deliberately omits everyone’s.
sealing at rest
Your identity is sealed into two independent copies, openable by either secret:
- via passphrase — key derived with Argon2id at the MODERATE profile (3 ops / 256 MiB; the unsealing has to run in a browser tab without OOMing a phone)
- via recovery code — a single BLAKE2b hash of the 32-byte code; no slow KDF needed because the code is already high-entropy
Which derivation runs is chosen by the type of secret you supply, never by a stored label — closing off algorithm-confusion downgrades.
trust-on-first-use, fail-closed
Clients pin every peer’s public keys on first sight. If a principal’s keys ever change, the pin
state flips to key_changed and clients treat that actor’s content as untrusted — rendered as a
sealed error card, never as trusted plaintext. A relay that swapped keys under a member would
break things loudly, not silently.