[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. 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. 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. 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.
[03]What the audit revealed
Here are the 9 major flaws the audit found, with the finding and its impact.
9 flaws shown
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
| Item | Before (PoC) | After (target) |
|---|---|---|
| Keys | Before (PoC) Private key in clear in localStorage | After (target)Non-extractable keys in IndexedDB |
| Passwords | Before (PoC) Blocking pbkdf2Sync | After (target)Asynchronous Argon2id |
| WebSocket session | Before (PoC) HTTP request every 30 s | After (target)Local JWT validation |
| Rooms | Before (PoC) creator / participant columns (1-on-1) | After (target)ConversationMembers table |
| Files | Before (PoC) 255-byte chunks in MySQL | After (target)S3 / MinIO object storage |
| Front-end state | Before (PoC) Global JS module variables | After (target)Pinia store, centralized Axios interceptors |
| Android permissions | Before (PoC) GPS, camera, microphone declared | After (target)INTERNET only, runtime requests |
| Secrets | Before (PoC) Versioned .env files, trivial values | After (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.