Skip to content

Case studyPRIVY.SYS2025

Privy, end-to-end encrypted messaging

What this project, audited without sparing myself, taught me about security and architecture.

  • 9major flawsJuly 2026 audit
  • 7categoriessecurity, performance, network…
  • 8fixeswhat I would do today
[ POST_MORTEM.LOG ]ARCHIVED • EDUCATIONAL POC

[01]Context

Privy came from the wish to design a confidential instant messenger without depending on a centralized giant. The goal was the zero-knowledge principle: servers were meant to be blind relays of encrypted packets, never able to read conversations or reach the symmetric keys.

Features: sign-up with a pseudonym and on-device key pair generation, secure invitations, real-time exchanges with typing indicators, an Android app through Capacitor and a draft of file sharing.

[02]Architecture

Vue 3 / Vite client with Pinia, wrapped as an Android app through Capacitor. Server side: an Express REST API (JWT authentication, profiles, invitations), a WebSocket server (ws) relaying encrypted messages, and MySQL 5.7, all orchestrated with Docker.

  1. 1. Identity

    At sign-up, the browser generates an RSA-OAEP 2048-bit key pair (SHA-256) through the Web Crypto API. The public key is sent to the API and stored on the server.

  2. 2. Envelope

    The initiator generates an AES-GCM 256-bit key, encrypts it with the recipient's public key and sends it in an invitation. The recipient decrypts it with their local private key.

  3. 3. Messages

    Each message is encrypted locally with AES-GCM, with a random 12-byte IV placed in front of the ciphertext, then Base64-encoded. The WebSocket server only receives an opaque blob.

Cryptographic flow: weak points (!) and fixes (→)DEVICE ARSA-OAEP 2048AES-GCM 256! private key inclear→ non-extractableIndexedDBSERVERREST APIWebSocketblind relay! HTTP every 30 s→ JWT validatedlocallyDEVICE BRSA-OAEP 2048AES-GCM 2561 publickey2 RSA envelope(AES)3 AES-GCM+ IV,Base64weak pointfix
The encryption flow. Weak points are marked "!" (pink), fixes "→" (orange).

[03]What the audit revealed

Here are the 9 major flaws the audit found, with the finding and its impact.

9 flaws shown

  1. FLAW 01 • SECURITY

    RSA private key in clear in localStorage

    The RSA private key and the conversation keys are written in clear in localStorage: a single XSS is enough to extract them and defeat end-to-end encryption.

  2. FLAW 02 • PERFORMANCE

    pbkdf2Sync blocks the Node.js event loop

    Password hashing uses pbkdf2Sync (1,000 iterations), which occupies Node.js's single thread on every call: under load, the API stops answering.

  3. FLAW 03 • NETWORK

    HTTP session revalidation every 30 s

    The WebSocket server has no access to the database or a shared cache: it revalidates each session with an HTTP request every 30 s, about 2,000 requests per minute for 1,000 clients, to check tokens that are already signed.

  4. FLAW 04 • ARCHITECTURE

    Relational model limited to 1-on-1

    The Conversations table relies on two columns, creator_id and participant_id: group rooms are impossible without rebuilding the schema and rewriting the controllers.

  5. FLAW 05 • PERFORMANCE

    O(R) disconnection over the number of rooms

    On disconnection, the WebSocket server scans every room in memory to detach the socket: a linear cost instead of a constant one.

  6. FLAW 06 • DATABASE

    Files split into 255-byte chunks in MySQL

    A 5 MB file would have required more than 20,000 rows in the database. The feature was dropped during development and its routes disabled.

  7. FLAW 07 • COMPLIANCE

    Over-provisioned Android permissions

    The manifest declared precise location, camera and microphone, added during experiments to fix a storage access problem: a violation of least privilege that would have been rejected on submission to the Google Play Store.

  8. FLAW 08 • RELIABILITY

    Stale token and endless reconnection

    The JWT was read only once, when the module was evaluated: after logging in, the other composables kept a null value and triggered 401 errors until the page was reloaded. The WebSocket also kept trying to reconnect every 5 s after a deliberate disconnection.

  9. FLAW 09 • SECURITY

    Versioned secrets, trivial values

    The .env files have been tracked by Git since the first commit, with trivial values. Deployed as is, this configuration would have allowed forging valid tokens for any user. I deliberately left them in place in the retrospective, as a visible example of a mistake I no longer make.

[04]What I would do today

If Privy were rewritten to current standards, this is what I would change.

What I would do today
ItemBefore (PoC)After (target)
KeysBefore (PoC) Private key in clear in localStorageAfter (target)Non-extractable keys in IndexedDB
PasswordsBefore (PoC) Blocking pbkdf2SyncAfter (target)Asynchronous Argon2id
WebSocket sessionBefore (PoC) HTTP request every 30 sAfter (target)Local JWT validation
RoomsBefore (PoC) creator / participant columns (1-on-1)After (target)ConversationMembers table
FilesBefore (PoC) 255-byte chunks in MySQLAfter (target)S3 / MinIO object storage
Front-end stateBefore (PoC) Global JS module variablesAfter (target)Pinia store, centralized Axios interceptors
Android permissionsBefore (PoC) GPS, camera, microphone declaredAfter (target)INTERNET only, runtime requests
SecretsBefore (PoC) Versioned .env files, trivial valuesAfter (target).env.example, strong injected secrets, CI detection

[05]What it triggered

The CCNA certification, the hardening of this portfolio (CSP with nonce, HMAC-signed deployment, network isolation) and the CVE watch built into the site.

[06]Limits

  • The project is archived: it is an educational PoC that must never be deployed to production.
  • It relies on obsolete dependencies (including MySQL 5.7, end of life) and has the flaws described above. The values in the versioned .env files are public.
// Last case studyBack to the projectsEvery project is on the homepage

Let's talk

A question about this work, or want to see more? Write to me.

Contact me