Commit Graph

1 Commits

Author SHA1 Message Date
Maciek "mab122" Bator e060442017 Add client-side encryption for private-repo LFS objects
Radicle's Visibility::Private is enforced entirely by access control at
the replication layer -- git objects for a private repo are plain,
unencrypted objects, protected only by "unauthorized peers are never
given them" (identity/doc.rs). That protection has no jurisdiction over
IPFS's own block-exchange layer: once a legitimate collaborator's IPFS
daemon pins content and joins the network, anyone who obtains the CID by
any means can fetch the plaintext directly.

Adds envelope encryption for private repos: a random per-object content
key encrypts the file (XChaCha20-Poly1305), wrapped once per authorized
recipient (delegates + private allow-list) via ECDH between Radicle
identity keys (already Ed25519, no separate keypair needed) plus HKDF.
The wrapped keys travel in the same refs/notes/rad-lfs note as the CID.

New:
- crates/radicle-cli/src/lfs_crypto.rs -- encryption core (envelope
  schema, encrypt/decrypt, key wrapping, recipient resolution, signer
  loading).
- `rad lfs store`/`rad lfs fetch` -- plumbing commands that now own the
  entire IPFS add/cat + git-notes read/write step (encrypting/decrypting
  as needed), replacing what the pre-commit hook and rad-lfs-transfer
  used to do directly. This collapses what would've become a third copy
  of the note-format logic down to one canonical implementation.
- `rad lfs rekey` -- since Radicle's access control is retroactive (a
  newly-authorized DID can replicate full history) but encrypted objects
  aren't automatically re-wrapped for new recipients, this command lets
  an already-authorized collaborator grant a newly-authorized one access
  to previously-committed objects, without re-encrypting or re-uploading
  content. Revocation remains unsolved, matching Radicle's own model
  (already-replicated/decrypted content can't be un-known).

Operational requirement: private-repo LFS operations need a signer
capable of ECDH, which only an unencrypted keystore or RAD_PASSPHRASE
provides (an ssh-agent-only signer can sign but not do key agreement).
Fails fast with an actionable error rather than hanging on a passphrase
prompt inside a non-interactive git hook.

Breaking change: the refs/notes/rad-lfs note format moves from bare
`cid=<cid>` text to JSON (`{"v":1,"cid":"...","enc":...}`). No migration
path -- prototype, no external users yet.

Verified end-to-end with real Radicle profiles/identities (not just the
transfer-agent protocol): a private repo's IPFS-stored content is
confirmed ciphertext (byte-different from plaintext, +16 bytes AEAD tag);
an authorized peer decrypts correctly; an unauthorized peer is denied
with a clear error and no leaked content; RAD_PASSPHRASE is required and
enforced with a fast, clear failure; and rekey correctly grants a
newly-authorized peer access to a pre-existing object while leaving other
recipients' entries untouched.
2026-07-14 16:01:49 +02:00