hoangsonww / hoangsonww/CollabNote-Fullstack-App
End-to-End Encryption (E2EE) for Notes & Sharing
- Dominant language
- TypeScript
- Stars
- 27
- Forks
- 11
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
Add **zero-knowledge, end-to-end encryption** so only clients can read note contents. The server (NestJS + DB + Supabase) stores only ciphertext + metadata. Supports **individual notes**, **shared notes**, **search by title (optional encrypted index)**, and **key rotation**.
---
### Goals
* Encrypt **note content + attachments** client-side (AES-GCM).
* Per-note random data key (DK), wrapped for each authorized user.
* Sharing: use X25519 (ECDH) or RSA-OAEP to wrap DKs to collaborators.
* Backward compatible via **feature flag** and **per-note `encryption_version`**.
* No plaintext leaves the browser; API never sees secrets.
---
### Non-Goals (v1)
* Server-side full-text search over ciphertext.
* Historic access for removed collaborators without re-share.
* Offline rekeying when all participants rotate simultaneously.
---
### Data Model (new tables/cols)
* `notes`
* `encryption_version smallint` (0 = none, 1 = aes-gcm)
* `ciphertext bytea` (instead of `content`)
* `nonce bytea`, `aad text` (optional)
* `content_hash text` (SHA-256 of plaintext for local diffing; never sent to server)
* `note_keys` (per note, per user)
* `note_id uuid FK`
* `user_id uuid FK`
* `wrapped_dk bytea` // DK encrypted with user’s public key
* `alg text` // e.g., `x25519-aesgcm`
* PK `(note_id, user_id)`
* `user_keys`
* `user_id uuid PK`
* `pub_key bytea` // X25519
* `pub_key_version int`
* (no private key stored server-side)
*Add RLS so only `user_id = auth.uid()` can read their `user_keys` row; `note_keys` gated by participant membership.*
---
### Crypto Plan (browser, WebCrypto)
* On first secure action, generate **X25519 keypair** (store private key in:
* **IndexedDB** primary, **WebCrypto non-extractable** if possible.
* Optional backup: user-entered passphrase → PBKDF2/scrypt → AES-GCM wrap and store encrypted private key in `user_secure_backup` (future).
* **Create/Update Note**:
1. Generate random 256-bit DK.
2. AES-GCM encrypt `{title, body, attachments[]}` blob with DK.
3. For author + each collaborator: ECDH(X25519) → shared secret → KDF → wrap DK into `wrapped_dk`.
* **Read Note**:
1. Fetch `note_keys` for current user → unwrap DK using private key.
2. Decrypt ciphertext; render.
* **Share Note**:
* When adding collaborator: fetch their `pub_key`, produce new `note_keys` row (no need to re-encrypt note body).
* **Rotation**:
* New user keypair? Rewrap DKs for that user only.
* Compromise? Rotate DK (re-encrypt note content once), rewrap to all members.
---
### API (additions)
* `GET /crypto/me/public-key` → `{ pubKey, version }`
* `POST /crypto/me/public-key` → register/rotate public key
* `POST /notes` → accepts `{ciphertext, nonce, aad, encryption_version, note_keys[]}`
* `PATCH /notes/:id` → same as above; server validates authorship/collab
* `POST /notes/:id/share` → `{ collaboratorUserId, wrapped_dk }`
* `DELETE /notes/:id/share/:userId` → revoke (delete `note_keys` row)
* `GET /notes/:id/key` → returns current user’s `wrapped_dk` (RLS enforced)
*Server validates shapes only; never touches plaintext.*
---
### Frontend (React/Vite)
* Crypto util: `crypto.ts`
* `ensureKeypair()`, `encryptNote()`, `decryptNote()`, `wrapDK()`, `unwrapDK()`
* Storage
* Private key in IndexedDB; fallback secure backup behind passphrase (optional v1.1).
* UI
* Badge on notes: `Encrypted`.
* Share modal: show who has access; when adding user, client computes and sends `wrapped_dk`.
* Migration banner to “Upgrade note to E2EE”.
* Error UX
* Missing keypair → prompt to generate.
* Unable to decrypt (revoked/wrong key) → “Request access” flow.
---
### Backward Compatibility / Migration
* Existing plaintext notes keep working.
* “Encrypt this note” action:
* Client fetches plaintext, generates DK, encrypts, writes `ciphertext`, sets `encryption_version=1`, populates `note_keys`.
* Feature flag: `E2EE_ENABLED` (env) + per-user toggle for early rollout.
---
### Acceptance Criteria
* [ ] Server never stores or logs plaintext note bodies or DKs.
* [ ] Owner can share/unshare; collaborators can read & edit (client re-encrypts on edit).
* [ ] Revoked collaborator cannot decrypt future edits (DK rotated or rewrapped policy).
* [ ] Tests: unit (crypto), integration (API shapes/RLS), e2e (create→share→read→revoke).
* [ ] Performance: encrypt/decrypt < 20ms for 50KB notes on mid-range device.
* [ ] Docs: README section + recovery caveats.
---
### Security Notes
* Use **AES-GCM** with 96-bit nonce; random nonces; store with ciphertext.
* AAD can include `{note_id, author_id, version}` to bind context.
* Derive wrapping key via **HKDF-SHA256(ECDH, salt=note_id)**.
* Block analytics on sensitive payload sizes; only count ops.
* Content scanning/virus checks for attachments must run **client-side** before upload (or accept that encrypted blobs are opaque to server scanners).
---
### Risks & Mitigations
* **Key loss** → optional passphrase-protected backup flow (follow-up issue).
* **Search** → offer “title left in plaintext” toggle or local encrypted index (future).
* **Invite race** → idempotent share endpoint; optimistic UI with server confirmation.
---
### Follow-ups
* Local encrypted search index (per user).
* Attachment E2EE streaming (chunked AES-GCM).
* Encrypted real-time cursor/ops (CRDT + shared DK).
Contributor guide
Assessment
This issue has not been assessed yet.