hoangsonww / hoangsonww/CollabNote-Fullstack-App

End-to-End Encryption (E2EE) for Notes & Sharing

Aperta
#7 1 commento 0 reazioni 2 assegnatari Rivendicata da @saurabh24thakur Vedi su GitHub
bug documentation duplicate enhancement good first issue help wanted question
Lingua principale
TypeScript
Stelle
27
Fork
11
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

### 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).

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.