Group/space multi-recipient encryption via per-group Keycloak identities #93

Merged
Cordy merged 4 commits from feat/multi-recipient into main 2026-08-03 19:21:56 +00:00
Owner

Closes #18

Approved by Manuel's review (2026-07-31, relayed by Nikola; single-recipient confirmed staying for personal files, 2026-08-03).

Design: unique key per group, mirroring the per-user model exactly. Every shared space gets its own age X25519 identity stored in the Keycloak group's attributes (same attribute name as users, lazily provisioned on the space's first encrypted operation). Content under /spaces/<name>/ encrypts to the group recipient (+ recovery); any member's read resolves the same group identity. Authorization stays with scope + the IdP — the group key provides confidentiality against the storage backend, so membership changes take effect instantly with zero re-encryption: joining grants the space's whole history, leaving revokes it.

Key-scope plumbing: the scope travels in the request context (WithGroup/GroupFrom), derived from the path by the storage decorators, and forwarded over the crypto-role wire as X-Cairn-Group alongside the existing X-Cairn-User — the ContentCrypter interface and both role deployments are untouched. (One wrinkle: Server.handle's encrypt bool parameter shadows the package name, hence the encscope import alias.)

Cross-boundary moves re-encrypt. Rename/Copy across the personal↔space (or space↔space) boundary decrypts with the source scope and encrypts to the destination scope, so a file moved into a space becomes member-readable and a copy taken out becomes private to the taker. Same-scope renames stay pure metadata moves (test-pinned: ciphertext byte-identical). This also makes trash coherent: deleting a space file re-keys it to the deleting user; restore re-keys it back to the space. Documented limitation: directory renames across the boundary pass through un-re-encrypted — move files, not folders, across scopes.

Tests: member-can-read/non-member-cannot, cross-scope rename+copy re-encryption, same-scope no-rewrite, and Keycloak group custody (lazy provision into group attributes + member decrypt roundtrip) against a fake Admin API.

Deployment note: the cairn-keycustody service account needs group read/manage in realm-management (manage-users normally covers it; add query-groups/view-users if group lookups 403 on the dogfood).

Closes #18 Approved by Manuel's review (2026-07-31, relayed by Nikola; single-recipient confirmed staying for personal files, 2026-08-03). **Design: unique key per group, mirroring the per-user model exactly.** Every shared space gets its own age X25519 identity stored in the **Keycloak group's attributes** (same attribute name as users, lazily provisioned on the space's first encrypted operation). Content under `/spaces/<name>/` encrypts to the group recipient (+ recovery); any member's read resolves the same group identity. Authorization stays with scope + the IdP — the group key provides confidentiality against the storage backend, so **membership changes take effect instantly with zero re-encryption**: joining grants the space's whole history, leaving revokes it. **Key-scope plumbing:** the scope travels in the request context (`WithGroup`/`GroupFrom`), derived from the path by the storage decorators, and forwarded over the crypto-role wire as `X-Cairn-Group` alongside the existing `X-Cairn-User` — the `ContentCrypter` interface and both role deployments are untouched. (One wrinkle: `Server.handle`'s `encrypt bool` parameter shadows the package name, hence the `encscope` import alias.) **Cross-boundary moves re-encrypt.** Rename/Copy across the personal↔space (or space↔space) boundary decrypts with the source scope and encrypts to the destination scope, so a file moved into a space becomes member-readable and a copy taken out becomes private to the taker. Same-scope renames stay pure metadata moves (test-pinned: ciphertext byte-identical). This also makes trash coherent: deleting a space file re-keys it to the deleting user; restore re-keys it back to the space. **Documented limitation:** directory renames across the boundary pass through un-re-encrypted — move files, not folders, across scopes. Tests: member-can-read/non-member-cannot, cross-scope rename+copy re-encryption, same-scope no-rewrite, and Keycloak group custody (lazy provision into group attributes + member decrypt roundtrip) against a fake Admin API. **Deployment note:** the `cairn-keycustody` service account needs group read/manage in realm-management (`manage-users` normally covers it; add `query-groups`/`view-users` if group lookups 403 on the dogfood).
Cordy merged commit 1e2c1ddb29 into main 2026-08-03 19:21:56 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#93
No description provided.