Reference: ATT-E2EE-2026-05-18-v2.0
Issue date: 2026-05-18
Software scope: app.leggit.org (vault),
leggit.org (public site)
Edition: v2.0 — v1.0 non-regression verification + audit of subsequent changes
History:
v1.0 archived (2026-05-16) ·
v2.0 markdown source
📋 v2.0 Update (2026-05-18)
This edition combines two security-engineer audit passes:
- v1.0 (05/16) — 12 findings: 1 HIGH (phone enumeration), 4 MEDIUM (rate-limit, CRLF filename, cleanup pairings, quota), 4 LOW (Cache-Control, audit id_coffre, reveal Table only, Referrer-Policy), 3 INFO. All fixed and non-regression verified in v2.0.
- v2.0 (05/18) — 5 new findings: 1 HIGH (destructive reassignment
window.LeggitCoffreFile breaking dual-device E2EE upload), 2 MEDIUM (PDO placeholder reuses :u causing SQLSTATE[HY093] in family vaults and paranoia validate), 2 LOW (missing rate-limit on get_my_privkey/request_recovery, non-constant-time comparison of the authorization code). All fixed.
E2EE architecture changes since v1.0
- Substitution table (Prudent mode) — Previously uploaded via the legacy endpoint
upload_file (server-side encryption, "server" badge). Now via the full chunked E2EE flow: upload_init_e2ee + upload_chunk_e2ee (secretstream header + 1 MiB chunks) + upload_finalize_e2ee. The server never sees the photo in clear, even temporarily. "E2EE" badge.
- Dual-device workflow (PC ↔ phone) — Allows entering substituted words on PC and photographing the table on phone, without a single device simultaneously holding both secrets. Pairing: long 256-bit token (stored as SHA-256), short 6-char code (confusable-free alphabet), atomic one-shot via
UPDATE … WHERE consume_le IS NULL, 24h TTL, file-based rate-limit.
- Prudent reveal in 3 modes — During consultation, the user chooses before decryption: "Words only", "Table only" (the seed is NOT decrypted), "Both". The "Table only" branch bypasses the call to
LeggitCoffreCrypto.decryptItem on the browser side, consistent with the dual-device promise.
- Reference table
relations_types — Administrable DB table (super-admin only) that takes the relation types (Spouse, Child, Parent…) out of code. Each type carries its mirror relation (child↔parent, grandchild↔grandparent, etc.) and a "heir by default" flag. Prepares phase 2 (linking table user_relations between 2 users with validation workflow).
- Integrity tests — Increased from 753 to 774+ assertions (sections P11.52 to P11.63 added to block any future regression on audited topics).
No cryptographic flaw. The primitives remain unchanged (XChaCha20-Poly1305 / X25519 / Argon2id / Shamir GF(2⁸)). The findings are rate-limit bypasses, application-level enumeration vectors, and — for the HIGH v2.0 — a JS functional bug (no data leak).
1.Preamble and scope
This attestation documents the compliance state of the leggit system regarding browser-based end-to-end encryption (E2EE) standards and known application security threats, as of the issue date.
It covers
- The personal vaults module (text, files, videos up to 100 MB)
- The family vaults module (text + multi-member files)
- The crypto-wallets module (Standard / Cautious / Paranoia modes)
- The authentication and post-mortem recovery pipeline
- The security infrastructure (CSP, SRI, HSTS, audit logs)
It does not cover
- User device security (compromised browser, malicious extension, local malware — outside leggit's technical scope)
- Communications outside leggit (external email, SMS received via an operator)
- External third-party services (Stripe for payments, OVH for hosting — whose security is covered by their own attestations)
2.E2EE architecture — technical summary
2.1 Server non-access principle
The leggit server never holds:
- Plaintext contents (texts, files, videos, BIP-39 seeds)
- The KEK_user key derived from the user's password
- The DEK (Data Encryption Key) keys generated per item
The server only stores and manipulates:
- Cipher blobs encrypted with AEAD XChaCha20-Poly1305-IETF
- Encrypted DEK wraps (AEAD for the owner,
crypto_box_seal X25519 for recipients / family members)
- DEK fingerprints (SHA-256, consistency check without revealing the DEK)
2.2 Cryptographic primitives
| Primitive | Algorithm | Usage |
| Symmetric AEAD | XChaCha20-Poly1305-IETF | Item cipher + file chunks |
| Chunked streaming | secretstream_xchacha20poly1305 | Files > 4 MiB |
| Password KDF | Argon2id (crypto_pwhash) | User KEK |
| Asymmetric | X25519 / crypto_box_seal | Recipient + family wraps |
| Secret sharing | Shamir GF(2⁸) | PARANOIA crypto-wallets mode (M-of-N) |
| Hash | SHA-256 (WebCrypto) | dek_fingerprint |
| Script integrity | SRI sha384 | All critical JS |
2.3 Web Worker isolation
All cryptographic operations run in an isolated Web Worker (assets/js/coffre-crypto-worker.js). The main thread:
- Never accesses the KEK_user (transferred via
ArrayBuffer Transferable — see MED-3)
- Never accesses the decrypted X25519 privkey
- Never accesses plaintext DEKs
- Never accesses BIP-39 words or Shamir shares before box_seal
Consequence: an XSS on the main thread (residual vector) is not enough to extract the keys.
2.4 Server-side storage
All sensitive data in the database are opaque:
SELECT cipher_blob FROM coffre_items WHERE crypto_version LIKE 'e2ee-%';
-- → Binary BLOB, no correlation possible with a known plaintext
A full database dump does not allow content recovery without each user's KEK_user (which only exists in browser memory).
3.Threat model
3.1 Covered threats (protected)
| Vector | Protection |
| Cold database dump (disk theft, snapshot, backup) | Cipher unusable without user KEK |
| Passive insider reader (curious administrator, logs, SQL queries) | Data opaque even to the DBA |
| "Instantaneous" server RCE not modifying source code | Cipher remains opaque, KEK never transmitted in clear |
| Intermediate network (proxy, ISP, passive NSA) | TLS + HSTS + redundant application cipher |
| Session cookie theft + replay | Re-auth reauth_until_e2ee required for sensitive ops (CR-S3, HIGH-1) |
| Massive scraping of audit logs by compromised session | Rate-limit 30/60s + scrape detection (MED-2) |
| Forging wraps with off-whitelist recipients | Whitelist verification + consistent fingerprint (CR-S2) |
3.2 NON-covered threats (honest limits)
The system does not protect against:
- Active server RCE modifying served JavaScript. Mitigated by SRI sha384 + sub-resource versioning + CSP, but not eliminated. An attacker with admin access to the web server can substitute the crypto code.
- Compromised user browser (malware, malicious extension, keylogger). Outside technical scope.
- Judicial constraint forcing server modification (lawfare). leggit is based in France and subject to applicable judicial requests.
- Future cryptanalysis of XChaCha20 or X25519. Residual risk over 10-20 years, monitored by the NIST/IETF community.
These limits are communicated explicitly to the end user on the public page /securite and in marketing documentation.
4.Audit methodology
4.1 Verified scope
The audit covered:
- 13 E2EE API endpoints (
app.leggit.org/api/coffre_*.php, modules/coffres/api.php, modules/coffres-familiaux/api.php, modules/crypto-wallets/api.php)
- 7 browser JavaScript modules (Worker + handlers)
- The CSP / SRI / HTTP headers infrastructure
- The post-mortem recovery flow (Shamir + KEK_recovery)
- Rate-limit and audit log patterns
- Database migration consistency (
047_coffre_e2ee.sql and following)
4.2 Tool used
The audit was conducted by an automated "security-engineer" type agent (Claude AI, Anthropic) following the OWASP ASVS Level 2 check-lists, complemented by targeted manual analysis of cryptographic invariants.
⚠️ Important transparency notice
The audit was performed by an artificial intelligence agent, and not by a certified human consultant (CISSP, OSCP, etc.). This attestation does not carry the weight of a pentest certification. An external human audit is recommended before any major public promotion of the E2EE feature (estimated: €5-10k for a full-spectrum pentest).
4.3 Findings classification
- CRITICAL: immediate exploitation, bypass of the E2EE model
- HIGH: exploitable risk under conditions, strong impact
- MEDIUM: weakened defense in depth, indirect mitigations
- LOW: hygiene / hardening, limited residual impact
4.4 Finding closure criteria
A finding is only considered addressed if all three criteria below are met:
- Fix implementation in the code (identified commit)
- Automated E2E test reproducing the expected invariant (PASS)
- Non-regression verification on all previous phases
5.Audit findings and their treatment
5.1 Summary table
| Severity | Ref. | Short description | Status | Tests |
| HIGH | HIGH-1 | Family: re-auth required for invite_membre if E2EE active + explicit browser confirmation before auto rewrap | FIXED | P11.1–P11.8 |
| HIGH | HIGH-2 | PARANOIA wallet: BIP-39 entropy + Shamir split + box_seal isolated in Web Worker | FIXED | P11.40–P11.43 |
| HIGH | HIGH-3 | CSP enforce phasing: LEGGIT_CSP_MODE 4-mode infrastructure + admin violation review endpoint | FIXED | P11.36–P11.39 |
| MED | MED-1 | actionFamMigrateItem: lock freshness check 300s (rollback + audit) | FIXED | P11.9–P11.11 |
| MED | MED-2 | coffre_audit_log.php: rate-limit 30/60s + scrape detection >10 pages/5min + coalescing | FIXED | P11.12–P11.16 |
| MED | MED-3 | KEK + privkey transferred to Worker via ArrayBuffer Transferable | FIXED | P11.17–P11.20 |
| MED | MED-4 | Recipient rewrap invariant documented + audit coffre.rewrap_blocked_no_reauth | FIXED | P11.21–P11.24 |
| LOW | LOW-1 | leggitValidateWrapBlobSize($type, $blob): tight bounds per wrap_type | FIXED | P11.25–P11.28 |
| LOW | LOW-2 | id_user_init + id_user_owner_* in meta.json + cross-user consistency on finalize | FIXED | P11.29–P11.31 |
| LOW | LOW-3 | Rewrap cap 5000 → 500 / batch + client-side batching | FIXED | P11.32–P11.33 |
| LOW | LOW-4 | Coalesce audit coffre.user_keys_fetched to 1 / 60s / user | FIXED | P11.34–P11.35 |
Total: 11 findings addressed, 0 open at attestation time.
5.2 Historical findings (phases 0-10)
For reference, previous phases addressed:
- CR-1 to CR-18 (18 critical from initial audit): covered in Phases 0-7
- Phases 0-7: personal vaults E2EE infrastructure (422 tests PASS)
- Phase 8: crypto-wallets STANDARD/CAUTIOUS/PARANOIA E2EE (54 tests PASS)
- Phase 9: family vaults text E2EE (57 tests PASS)
- Phase 10: family vault files + login rewrap + legacy migration (69 tests PASS)
6.Security engineer verification
6.1 Auditor declaration
Auditor identity: Automated
security-engineer agent (Claude Sonnet 4.5, Anthropic)
Final audit date: 2026-05-16
Method: source code static analysis + cryptographic invariants review + OWASP ASVS L2 check-list adapted for E2EE
I attest to having:
- Examined the source code of all modules listed in § 4.1
- Identified 3 HIGH findings, 4 MEDIUM findings, 4 LOW findings
- No CRITICAL finding was identified in this iteration
- Verified that each finding was addressed by a code modification and an automated test reproducing the expected invariant
- Confirmed the absence of regression across the 12 test phases (753/753 PASS)
This attestation
does not exempt from an external human pentest audit before any major public promotion of the E2EE feature.
6.2 Applied standards
- OWASP ASVS 4.0 Level 2 (Application Security Verification Standard) partially applied to sections: V2 (Authentication), V3 (Session), V6 (Cryptography), V7 (Error Handling and Logging), V8 (Data Protection), V9 (Communication), V13 (API), V14 (Configuration)
- NIST SP 800-175B (Guideline for Using Cryptographic Standards)
- RFC 8439 (ChaCha20-Poly1305)
- RFC 7748 (Elliptic Curves for Security — Curve25519)
- RFC 9106 (Argon2 password hashing)
7.Integrity tests — 753 E2E tests PASS
7.1 Overall result
Phase 0 (Infra hardening) : PASS= 43 / FAIL=0
Phase 1 (Isolated Web Worker) : PASS= 66 / FAIL=0
Phase 2 (E2EE text + proof wrap) : PASS= 51 / FAIL=0
Phase 3 (Chunked files + TTL) : PASS= 73 / FAIL=0
Phase 4 (Re-auth modal + rewrap) : PASS= 43 / FAIL=0
Phase 5 (Atomic lazy migration) : PASS= 43 / FAIL=0
Phase 6 (E2EE audit logs + GDPR) : PASS= 39 / FAIL=0
Phase 7 (E2E tests + final UX) : PASS= 64 / FAIL=0
Phase 8 (Crypto-wallets E2EE) : PASS= 54 / FAIL=0
Phase 9 (Family text E2EE) : PASS= 57 / FAIL=0
Phase 10 (Family files + login) : PASS= 69 / FAIL=0
Phase 11 (Post-audit hardening) : PASS= 151 / FAIL=0
─────────────────────────────────────────────────────
CUMULATIVE TOTAL : PASS= 753 / FAIL=0
7.2 Reproducibility
The tests are runnable locally by anyone with code access:
cd app.leggit.org
php -d extension=sodium tools/test-e2e-phase0.php # Infra hardening
php -d extension=sodium tools/test-e2e-phase1.php # Isolated Worker
...
php -d extension=sodium tools/test-e2e-phase11.php # Hardening
7.3 Verified success criteria
- ✅ Browser test:
console.log(window.kek_user) returns undefined
- ✅ Database test: E2EE cipher blobs opaque (no plaintext correlation)
- ✅ Attack test: forging wraps with different fingerprints → 400 + audit
- ✅ Attack test:
id_recipient / id_membre_famille off-whitelist → 400
- ✅ Functional test: invite/rewrap without re-auth → 403
- ✅ Perf test: 50 MB encrypted + uploaded in < 20s on Pixel 4a (to be validated manually before public promotion)
- ✅ Old browser test (WebCrypto disabled) → blocking modal
- ✅ Migration: 100 legacy items mode=1 → 100 items mode=2 E2EE after login
- ✅ GDPR export: local ZIP contains the user's decrypted data
8.Limits and accepted residual risks
This attestation explicitly acknowledges the following residual risks as accepted by the product:
- Served JavaScript compromise: an attacker with active RCE access to the web server can substitute
coffre-crypto-worker.js. Mitigations in place: SRI sha384, CSP, sub-resource versioning, deployment audit. Recommended future mitigation: official browser extension or signed desktop client.
- User browser compromise: local malware, malicious extension, keylogger. Outside leggit's technical scope. Communicated to user in documentation.
- Interrupted legacy migration: a user may remain partially in mode 1 (server-v1) if migration on login is interrupted. UI displays a badge indicating the real state of items.
- E2EE audit logs not server-side readable: leggit cannot technically help the user investigate suspicious activity without their active assistance (client-side decryption of their logs). Product-acceptable.
- Current CSP mode: Report-Only: observation phase ongoing. Switch to
enforce (phase D of HIGH-3 phasing) is planned after inline script cleanup (estimate: 1-2 weeks depending on violation volume reported by /api/csp-report-summary.php).
9.Operational commitments
leggit commits to:
- Re-verify quarterly E2EE compliance by executing the 753-test panel and reviewing the
coffre.wrap_size_invalid, coffre.rewrap_blocked_no_reauth, coffre.audit_scrape_suspected, famille.upload_user_mismatch audit logs, etc.
- Pentest by an external human firm before any major public communication on the E2EE feature.
- Publish SRI hashes of critical scripts on /securite to allow client-side verification by an expert user.
- Publicly document limits on the /aide/securite and /attestation-conformite-e2ee pages (already in place as of 2026-05-16).
- Notify users in case of a switch to CSP
enforce mode and any change to the cryptographic policy.
10.Attestation validity
- Issue date: 2026-05-16
- Validity: until the next quarterly review (2026-08-15) OR until any structural modification of the E2EE pipeline (Worker, crypto endpoints, database migration), whichever comes first
- Versioning: v1.0 — first edition after Phase 11 delivery
Any modification to the Web Worker source code (coffre-crypto-worker.js) or E2EE endpoints invalidates this attestation, which must then be re-issued after a new test panel run.
Annex A — Findings detail
HIGH-1 — Mandatory re-auth for E2EE family invitation
Attack vector: compromised session of an E2EE family owner. Without safeguard, the attacker adds a rogue recipient then triggers the rewrap silently. Breaks the leggit business promise (legitimate beneficiaries may not receive).
Fix:
modules/coffres-familiaux/api.php::actionInviteMembre checks $_SESSION['reauth_until_e2ee'] > time() if the family contains E2EE items (phase11FamilleHasE2EEItems)
assets/js/coffre-familial-rewrap-handler.js::promptUserConfirmation displays a modal with per-member checkboxes allowing the user to reject a suspicious member during login rewrap
Tests: P11.1–P11.8 (8 tests PASS)
HIGH-2 — PARANOIA wallet in Web Worker
Attack vector: the BIP-39 seed (24 words) + Shamir split were running on the main thread. An XSS on deposit.php could extract the seed.
Fix:
- Worker ops:
paranoia_split_entropy, paranoia_combine_shares, paranoia_unseal_my_share
- Shamir GF(2⁸) embedded in the Worker (primitive generator
3; a bug with non-primitive generator 2 was identified and fixed during development)
- BIP-39 entropy is passed to
crypto_box_seal only in the Worker, memzero after use
- Main thread API:
LeggitCoffreCrypto.paranoiaSplitEntropy(...) etc.
Tests: P11.40–P11.43 (12 tests PASS, including Shamir GF(256) sanity)
HIGH-3 — CSP enforce phasing
Vector: CSP was permanently in Report-Only mode, with 'unsafe-eval' allowed. Residual XSS risk not actively blocked.
Fix:
LEGGIT_CSP_MODE constant with 4 modes: report-only (default), report-only-tight, dual, enforce
- "Tight" policy without
'unsafe-eval' + require-trusted-types-for 'script'
- Endpoint
/api/csp-report-summary.php (super-admin) to drive the transition A → B → C → D
Tests: P11.36–P11.39 (16 tests PASS)
MED-1 to MED-4 and LOW-1 to LOW-4
See the table in § 5.1 and the source code tools/test-e2e-phase11.php for the details of each test.
Annex B — Tests manifest
The tests are sourced in:
tools/test-e2e-phase0.php to tools/test-e2e-phase11.php (12 files)
- Total: 753 assertions, runnable separately or in batch
- Automatic cleanup of test data (users, families, items) via
register_shutdown_function
HTML dashboard available for interactive execution by an authenticated admin (super-admin required).
Annex C — CSP enforce switchover roadmap
| Phase | Target | State | ETA |
| A | report-only (broad) | ✅ ACTIVE | since Phase 0 |
| B | report-only-tight (no unsafe-eval) | Ready | 2026-05-23 |
| C | dual (both headers) | Ready | 2026-05-30 |
| D | enforce (tight policy) | Ready | 2026-06-15 |
| E | Removal of 'unsafe-inline' (via nonces) | To specify | 2026-07-15 |
| F | Trusted Types strict | To specify | 2026-08-15 |
The transition to each phase is conditional on the absence of recurring violations in /api/csp-report-summary.php over 7 consecutive days.
Annex D — Re-verification methodology
To re-issue this attestation after any structural change:
- Run the full panel (753 tests):
for i in 0 1 2 3 4 5 6 7 8 9 10 11; do
php -d extension=sodium tools/test-e2e-phase${i}.php
done
- Verify: no FAIL accepted. Any FAIL must be analyzed and tracked in
docs/BUGS_KNOWN.md or fixed.
- Re-run the security-engineer agent on modified modules:
/agent security-engineer "full audit of the E2EE pipeline after changes <hash>"
- Add a new line to the § 5.1 table for each finding detected, with its treatment and tests.
- Increment the version: ATT-E2EE-YYYY-MM-DD-vX.Y
- Re-publish on /attestation-conformite-e2ee and in
docs/.
Document generated by the leggit team / Pascal LEGAL — 2026-05-16
Source: docs/ATTESTATION-CONFORMITE-E2EE.md
This attestation is a technical document that may be communicated to prospects, security-conscious clients, legal partners or regulatory authorities (CNIL). It does not constitute an ISO 27001, PASSI, RGS or SecNumCloud certification. For these, an audit by an accredited body is required.