diff --git a/DESIGN-E2EE.md b/DESIGN-E2EE.md new file mode 100644 index 0000000..ee1666c --- /dev/null +++ b/DESIGN-E2EE.md @@ -0,0 +1,160 @@ +# End-to-End Encryption (E2EE) Design — Option A Sign-off + +Status: Ready for sign-off +Owner: Kyaw + +## 1. Scope and Goals +- Web-first PWA with future native iOS/Android. +- Protect messages, files/assets (3D/logo/media), docs, and analytics events. +- Privacy-preserving analytics client-side; server-side limited to minimized metadata. +- Retrofit behind feature flags; backward-compatible bridging for non-E2EE content. + +## 2. Threat Model +- Adversaries: curious/compromised servers, external attackers (MITM), malicious clients, stolen devices. +- Trust boundaries: Only clients see plaintext/keys. Servers handle ciphertext, minimal metadata, and capability tokens. IdP proves identity only. +- Security goals: Confidentiality & integrity; forward secrecy and post-compromise security; deniable auth for messages; verifiable membership/state. +- Out of scope initial: traffic analysis resistance; plaintext content scanning; hardware tamper beyond platform enclaves. + +## 3. Cryptographic Primitives and Libraries + +### 3.1 Argon2id Parameters (desktop vs mobile) +- Desktop/Web: m=64 MiB, t=3, p=1 — balances user-perceived latency with robust GPU/ASIC resistance for key wrapping. Typical derivation latency ~200–500 ms on contemporary laptops. +- Mobile: m=32 MiB, t=3, p=1 — reduces memory pressure and thermal impact while keeping meaningful resistance. Target latency ~300–700 ms depending on device class. +- Salt length: 16 bytes (random per vault). Output length: 32 bytes (KEK). +- Rationale: Memory-hardness is the dominant cost lever; t=3 provides reasonable compute amplification without excessive energy draw on battery devices. + +- Ed25519 (signing) for identities and devices. +- X25519 (ECDH) for key agreement; sealed boxes for key wrapping (HPKE-ready abstraction). +- Messaging: Signal/Double Ratchet via libsignal-client (WASM) for 1:1/small groups. +- Large groups: Signal sender keys with periodic rotation; MLS on roadmap. +- Files: AES-256-GCM streaming with HKDF-derived per-chunk nonces; BLAKE3 for chunk and whole-file digests. +- KDF: HKDF-SHA256; Password KDF: Argon2id (m=64 MiB, t=3, p=1; mobile fallback m=32 MiB). Salt: 16 bytes. Output: 32 bytes. +- Hashing: BLAKE3 for content addressing and integrity. + +## 4. Identity & Device State Machines +### 4.1 Identity Keys +States: uninitialized -> generated -> backed_up (optional) -> compromised(revoked) +Transitions: +- generate: create Ed25519 identity key pair +- backup: wrap private key with Argon2id-derived KEK; store vault in IndexedDB +- revoke: mark identity compromised; re-enroll devices + +### 4.2 Device Enrollment +States: new -> pending_attestation -> verified -> revoked +Transitions: +- new: device generates Ed25519 (sign) + X25519 (DH) +- provision: QR shows {device_pubkeys, nonce}; trusted device scans and verifies SAS +- attest: trusted device signs attestation binding device to identity +- verify: server records attestation; device becomes verified +- revoke: immediate revocation; triggers rotations + +## 5. Messaging Sessions +- 1:1 and small groups: Double Ratchet with prekeys from libsignal. +- Device revocation: peers refuse messages from revoked devices. +- Group sender keys: per-room sender key rotated on membership change and every 7 days; per-recipient key wraps. + +## 6. File Encryption and Sharing +See also Mermaid diagrams in docs/diagrams for provisioning, file-share, and rekey flows. + +### 5.1 Canonicalization (signatures & tokens) +- Manifest signing: Canonical JSON per RFC 8785 (JSON Canonicalization Scheme, JCS). Remove the `sig` field before canonicalization; sign the result with device Ed25519. Verification recomputes canonical JSON and verifies the signature. +- PASETO payload normalization: When computing or verifying detached request signatures or audit hashes, canonicalize the JSON payload using the same JCS rules and sort header fields if applicable. Avoid including transient fields (e.g., `iat` skew-adjusted values) in hash commitments. + +### 6.1 Streaming Encryption +- Per-file random DEK (256-bit). +- Chunk size 512KB–2MB (adaptive). Max single-object size: 5 GB (resumable uploads). +- Nonce derivation: nonce_i = HKDF(DEK, info="file-chunk" || chunk_index)[0..12] +- AES-256-GCM over each chunk; produce per-chunk BLAKE3 and cumulative whole-file BLAKE3. + +### 6.2 Manifest Format (signed by device Ed25519) +``` +version: 1 +algo: aes-256-gcm +chunk_size: +length: +blake3_file: +chunks: + - index: 0 + offset: 0 + size: + blake3: + - ... +key_wraps: omitted in manifest; stored adjacent by object_id +sig: ed25519(signing_device_pubkey, JCS(manifest_without_sig)) +``` + +### 6.3 DEK Sharing +- For each recipient device X25519 pubkey, create sealed box of DEK. +- Store wraps: key_wraps(object_id, device_id, wrap_ciphertext) +- Rekey on membership change; rewrap to active devices. + +## 7. Capability Tokens +- Format: PASETO v4.public (Ed25519-signed by server capability key). +Claims: +- sub: user or device id +- scope: [object:get|put, room:read, room:write, membership:manage] +- resource: URI or prefix (e.g., s3://bucket/path/object-id) +- exp: expiry; iat/nbf +- region: enforced data residency region (controls bucket/prefix routing) +- tid/nonce: unique token id to prevent replay +- TTLs: object GET/PUT 15 minutes; room/event scopes 1 hour. + +## 8. APIs (Server) +- POST /devices/attest +- POST /devices/revoke +- POST /rooms +- POST /rooms/:id/members +- POST /rooms/:id/rotate +- POST /capabilities +- PUT /objects/:id (requires capability) +- GET /objects/:id (requires capability) +- WS /events + +## 9. Storage Schema (Postgres + S3-compatible) +Residency enforcement: region claim in capabilities is validated and mapped to storage bucket/prefix; mismatches are rejected at capability validation and storage routing. +- users(id, identity_pubkey_hash, oidc_sub, region) +- devices(id, user_id, ed25519_pub, x25519_pub, attestation_sig, status) +- rooms(id, created_by, policy) +- memberships(room_id, device_id, role, since, status) +- sender_keys(room_id, epoch, key_id, wrapped_keys jsonb, created_at) +- objects(id, owner, room_id, bucket, path, blake3_digest, size, manifest_sig, created_at) +- key_wraps(object_id, device_id, wrap_ciphertext) +- audit_events(id, actor, type, target, ts, meta) + +## 10. Backup & Recovery +SAS & QR: Use emoji SAS (7-emoji sequence from a 64-emoji table) for human-friendly verification. QR payload schema: base64url(JSON { device_pubkeys, nonce, sig, ts }). +- Key vault: private keys wrapped by Argon2id-derived KEK; IndexedDB on web; Secure Enclave/Keystore on mobile. +- Optional Shamir 2-of-3 recovery (user + admin escrow + HSM) with approvals and audit. + +## 11. Metadata Minimization +Audit taxonomy and retention: capture key lifecycle events, membership changes, capability issuance/revocation, and recovery attempts. Retention: 1 year (then purge or anonymize per policy). +- Store hashed identity references; coarse timestamps; encrypted membership maps when feasible. +- Avoid plaintext titles/tags. No plaintext in logs. + +## 12. Request Signing & Replay Protection +- Client signs sensitive requests with device Ed25519 over canonical payload + timestamp. +- Server enforces skew window and tid uniqueness. + +## 13. Performance Targets +- p95 decrypt < 120 ms for 10 MB on desktop. +- Streaming crypto in Web Workers; backpressure-managed I/O. + +## 14. Rollout & Kill Switch +- Feature flags per tenant/room. +- Canary cohorts; schema uses sidecar tables for isolation. +- Instant kill-switch disables capability issuance for E2EE objects/rooms; existing ciphertext remains intact. + +## 15. CI/CD and Supply Chain +- Renovate/Dependabot with grouped patch/minor; majors manual. +- GitHub Actions: lint/typecheck/tests, CodeQL, SCA, SBOM (Syft), container scanning. +- Signed commits and releases. + +## 16. Test Plan (Acceptance Gates) +- Unit and property tests for: keygen, provisioning, sealed box wraps, AES-GCM streaming (vectors), manifest sign/verify, PASETO claims/validation, rotation flows. +- Integration: 1:1 E2EE chat, file upload/download, membership change triggers rewrap/rotation. +- Data residency pinning tests; GDPR DSR exercises. + +## 17. Open Items / Future Work +- Evaluate MLS migration path for large rooms. +- HPKE support behind wrapping abstraction. +- Privacy-preserving analytics with DP budget management per org. diff --git a/docs/diagrams/file-share.mmd b/docs/diagrams/file-share.mmd new file mode 100644 index 0000000..451898b --- /dev/null +++ b/docs/diagrams/file-share.mmd @@ -0,0 +1,19 @@ +sequenceDiagram + autonumber + participant C as Client + participant O as Object Store + participant S as Server + + C->>C: Generate DEK + loop Encrypt chunks + C->>C: nonce_i = HKDF(DEK, "file-chunk"||i)[0..12] + C->>C: c_i = AES-GCM(plaintext_i, nonce_i) + C->>C: blake3_i = BLAKE3(c_i) + end + C->>C: blake3_file = BLAKE3(c_*) ; manifest+sig + C->>O: PUT chunks + manifest (ciphertext only) + par share DEK + C->>S: POST key_wrap(device_pkX, sealed_box(DEK)) + and for each recipient + C->>S: POST key_wrap(device_pkY, sealed_box(DEK)) + end diff --git a/docs/diagrams/provisioning.mmd b/docs/diagrams/provisioning.mmd new file mode 100644 index 0000000..138b889 --- /dev/null +++ b/docs/diagrams/provisioning.mmd @@ -0,0 +1,13 @@ +sequenceDiagram + autonumber + participant ND as New Device + participant TD as Trusted Device + participant S as Server + + ND->>ND: Generate Ed25519_sign_D, X25519_D, nonce N, ts + ND->>TD: Display QR: base64url(JSON{device_pubkeys, N, ts, sig=Sign_D(N||ts)}) + TD->>TD: Compute SAS = hash(identity_pub, device_pubkeys, N, ts) -> 7 emojis + ND-->>TD: Human verifies SAS matches + TD->>S: POST /devices/attest(attestation=Sign_T{identity_pub, device_pubkeys, N, ts}) + S-->>TD: 200 OK + S-->>ND: WS event device-verified diff --git a/docs/diagrams/rekey.mmd b/docs/diagrams/rekey.mmd new file mode 100644 index 0000000..70f15c5 --- /dev/null +++ b/docs/diagrams/rekey.mmd @@ -0,0 +1,11 @@ +sequenceDiagram + autonumber + participant A as Admin + participant S as Server + participant C as Clients (room) + + A->>S: Update membership + S-->>C: WS membership-change + C->>C: Rotate sender key; rewrap DEKs + C->>S: POST new wraps + S-->>C: Ack; old wraps invalidated diff --git a/docs/examples/paseto-v4-public.json b/docs/examples/paseto-v4-public.json new file mode 100644 index 0000000..62e39fc --- /dev/null +++ b/docs/examples/paseto-v4-public.json @@ -0,0 +1,14 @@ +{ + "v": 4, + "purpose": "public", + "claims": { + "sub": "device:9b1d2...", + "scope": "object:get", + "resource": "s3://brand-intel-eu/tenant123/objects/abc123", + "exp": 1723939200, + "iat": 1723935600, + "nbf": 1723935600, + "region": "eu", + "tid": "d7b2f0f8-7e2a-4d31-8b4e-7e7c2f0b9d51" + } +} diff --git a/docs/schemas/capability.schema.json b/docs/schemas/capability.schema.json new file mode 100644 index 0000000..f4e27e5 --- /dev/null +++ b/docs/schemas/capability.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/capability.schema.json", + "title": "Capability Claims (PASETO v4.public payload)", + "type": "object", + "required": ["sub", "scope", "resource", "exp", "region", "tid"], + "properties": { + "sub": {"type": "string"}, + "scope": {"type": "string", "enum": ["object:get", "object:put", "room:read", "room:write", "membership:manage"]}, + "resource": {"type": "string"}, + "exp": {"type": "integer"}, + "iat": {"type": "integer"}, + "nbf": {"type": "integer"}, + "region": {"type": "string", "description": "enforced data residency region (controls bucket/prefix routing)"}, + "tid": {"type": "string"} + } +} diff --git a/docs/schemas/manifest.schema.json b/docs/schemas/manifest.schema.json new file mode 100644 index 0000000..8f15bc7 --- /dev/null +++ b/docs/schemas/manifest.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/schemas/manifest.schema.json", + "title": "E2EE File Manifest", + "type": "object", + "required": ["version", "algo", "chunk_size", "length", "blake3_file", "chunks", "sig"], + "properties": { + "version": {"type": "integer", "enum": [1]}, + "algo": {"type": "string", "const": "aes-256-gcm"}, + "chunk_size": {"type": "integer", "minimum": 262144, "maximum": 4194304}, + "length": {"type": "integer", "minimum": 0}, + "blake3_file": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "chunks": { + "type": "array", + "items": { + "type": "object", + "required": ["index", "offset", "size", "blake3"], + "properties": { + "index": {"type": "integer", "minimum": 0}, + "offset": {"type": "integer", "minimum": 0}, + "size": {"type": "integer", "minimum": 1}, + "blake3": {"type": "string", "pattern": "^[0-9a-f]{64}$"} + } + } + }, + "sig": {"type": "string", "description": "Ed25519 signature over canonicalized manifest without sig"} + } +} diff --git a/docs/sequences/e2ee-sequences.md b/docs/sequences/e2ee-sequences.md new file mode 100644 index 0000000..6f8e904 --- /dev/null +++ b/docs/sequences/e2ee-sequences.md @@ -0,0 +1,32 @@ +# E2EE Sequence Diagrams (Textual) + +## 1) Device Provisioning (QR + SAS) +1. New device: gen (Ed25519_sign_D, X25519_D), create nonce N and ts +2. New device -> QR payload: base64url(JSON { device_pubkeys, nonce: N, ts, sig: sign_D(N||ts) }) +3. Trusted device scans QR +4. Both devices derive SAS from transcript (hash of {identity_pubkey, device_pubkeys, N, ts}) -> 7 emojis +5. Human verifies SAS match +6. Trusted device signs attestation: sign_T({ user_identity_pub, device_pubkeys, N, ts }) +7. Trusted device -> Server: POST /devices/attest(attestation) +8. Server records device; broadcasts WS event + +## 2) File Share (Encrypt + Upload + Share DEK) +1. Client picks file; gen random DEK +2. For each chunk i: + - nonce_i = HKDF(DEK, info="file-chunk"||i)[0..12] + - c_i = AES-256-GCM(plaintext_i, nonce_i) + - blake3_i = BLAKE3(c_i) +3. blake3_file = BLAKE3(c_0||c_1||...) +4. Manifest = { version, algo, chunk_size, length, blake3_file, chunks[] } +5. sig = Ed25519.sign(manifest_without_sig) +6. Upload chunks and manifest (ciphertext only) +7. For each recipient device pk_X: + - wrap = sealed_box(DEK, pk_X) + - POST key_wraps(object_id, device_id, wrap) + +## 3) Membership change -> Rekey +1. Admin changes room members +2. Server emits WS membership-change event +3. Clients rotate sender key and/or rewrap DEKs +4. New wraps stored; old wraps invalidated +5. Capability issuance follows narrow scopes with TTL