NodeCrypt: a browser chat where the server only ever sees ciphertext
🔒True end-to-end encrypted (E2EE) chat system in web. 🔒真正的网页端到端加密聊天系统
At a glance
- What is it?
- NodeCrypt is an ISC licensed JavaScript chat system that runs on Cloudflare Workers and Durable Objects and keeps every message encrypted in the browser, using RSA-2048 for server identity, P-384 ECDH for the transport and Curve25519 plus a room password for the conversations. It suits people who want a real-time chat that their hosting provider cannot read, and its whole security story rests on a handful of JavaScript cryptography libraries.
- Who is it for?
- NodeCrypt fits a user who wants a small real-time chat with no accounts, no history and no server able to read the messages, and the deployment story is short because the whole backend is a Cloudflare Worker with Durable Objects.
- Can I use it commercially?
- Yes. ISC is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 67 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The server as a blind relay, and what that rules out
NodeCrypt is presented as a true end-to-end encrypted chat system with a zero-knowledge architecture, and the design principle it states is that the server acts as a blind relay for encrypted data.
The README is specific about the properties that follow. There is no persistent storage, the system uses no durable storage, and all data exists only temporarily in memory. There is forward secrecy, in the sense that even if a key leaks, historical messages cannot be decrypted, because there are no historical messages to decrypt. Communication is anonymous, users need not register a real identity, and temporary anonymous chat is supported.
That last set of properties is a consequence of the architecture rather than an addition to it. If the server keeps nothing, there is nothing to subpoena, nothing to leak, and no history to compromise. The design also means the room membership list is the one thing the server necessarily knows, since it has to route messages, and the README is candid that the online list in a room is fully transparent, with any member joining or leaving notified to all members in real time. A newly joined user cannot see any history.
The private message feature is described in the same terms. Clicking a user's avatar starts an end-to-end encrypted private conversation that other members of the room cannot see at all.
So the claim to evaluate is not that the system is encrypted, since everything is, but that the server is structured so that decrypting a message would require a capability the server does not have. That is the design goal, and whether the implementation meets it is a question about the code, which the README explicitly invites researchers to audit: all encryption related code is open source, it uses standard cryptographic algorithms, and the project welcomes security researchers reporting vulnerabilities, with serious issues to be fixed within 24 hours.
Three layers: server identity, transport keys, message keys
The security system is described as three layers, and reading them in order makes the whole design legible.
The first layer is RSA-2048 server identity verification. The server generates an ephemeral RSA-2048 key pair when it starts and rotates it automatically every 24 hours. When a client connects it verifies the server's public key, which is the step that blocks a man in the middle. The private key exists only in server memory and is never persisted.
The second layer is ECDH key agreement on the P-384 curve. Each client generates its own elliptic curve key pair, and the shared key is established through Elliptic Curve Diffie-Hellman key exchange, giving every client an independent encrypted channel with the server.
The third layer is hybrid symmetric encryption, and it splits by direction. Communication with the server uses AES-256-CBC for the control messages, and communication between clients uses ChaCha20 for the actual chat content. Each message uses its own initialization vector and random nonce.
The full sequence is laid out in a mermaid sequence diagram with seven stages: server identity verification, client to server key exchange, room authentication, client to client key exchange on Curve25519, password-enhanced key derivation, mutual identity verification, and finally secure message transport with double encryption where the content is ChaCha20 encrypted and then wrapped in an AES-256 transport layer. The server's role at that last stage is to unwrap the AES layer and forward the still encrypted payload, which it cannot read.
One point of terminology in the diagram deserves a note, because it is easy to misread. The stage four exchange is on Curve25519 while stage two is on P-384, and the two serve different purposes: P-384 is for the client to server channel, Curve25519 is for the client to client conversations.
The room password is XORed into every shared key
The room password mechanism is the part of the design with the most surprising property, and it is worth working through carefully because it defines the security boundary.
The README gives the formula directly: the final shared key equals the ECDH shared secret XOR SHA256 of the room password. The room password acts as a key derivation factor in the end-to-end encryption.
Two properties follow from that. The first is wrong password isolation, since rooms with different passwords cannot decrypt each other's messages. The second is a server blind spot, because the server can never learn the room password.
The stage five notes in the sequence diagram spell out the per-pair derivation: the client key is the ECDH over Curve25519 of your own private key and the other party's public key, XOR SHA256 of the password, and the same computation happens on the other side, so both parties arrive at the same key without the server ever seeing the password or the plaintext.
Now the honest reading of the security advice section. The README says to use a strong room password, because the room password directly affects the strength of the end-to-end encryption, and it says to keep the password confidential, because once the room password leaks all communication in that room may be decrypted. It also recommends a current modern browser, to make sure the cryptography API is both safe and fast.
That last point is the one to sit with. The password is not an additional layer on top of the key exchange; it is a factor XORed into it, and it is the only human-chosen secret in the system. Everyone in the room must know it, which means it is shared over whatever channel the users already trust, and the README does not describe an in-band way of distributing it. A strong password protects against an attacker who captures a key exchange; it does not protect against a participant who was always going to have the key.
What the browser does, and which libraries do the work
The technical implementation section lists the libraries, and the list is short enough to be a fair summary of the trust surface.
The Web Cryptography API provides the browser's native cryptographic implementation with hardware acceleration. elliptic.js is the elliptic curve library implementing Curve25519 and P-384. aes-js is a pure JavaScript AES implementation supporting multiple modes. js-chacha20 is the JavaScript implementation of the ChaCha20 stream cipher, and js-sha256 is the SHA-256 hash implementation.
The package.json confirms that in the dependency list, with aes-js, elliptic, js-chacha20 and js-sha256 all present alongside utilities such as fflate for compression, dompurify for sanitising, emoji-picker-element and the two @dicebear packages for avatars.
The interesting split is between aes-js and the Web Cryptography API. The README lists the Web Cryptography API as providing native implementation with hardware acceleration, and aes-js as a pure JavaScript implementation, so both paths exist in the code, and the pure JavaScript fallback is what would run in a context where the native API is unavailable.
On the front end, the README describes ES6+ modular JavaScript with no framework dependency, which is a deliberate constraint. The build is Vite, and the client-side dependencies are the same crypto libraries rather than a bundling of a larger framework, so the client bundle is essentially the crypto code plus the app.
The server side is Cloudflare Workers plus Durable Objects, with WebSocket for real-time bidirectional communication. The Durable Objects part matters for the architecture: they give each room a single-threaded, stateful isolate with its own memory, which is what makes a no-database design workable, since the room's membership list and routing state live inside the object rather than in a store. The Dockerfile in the repository root shows the alternative path, a Node.js 18 Alpine builder with an nginx stage serving the static client, which is useful if you would rather not deploy to Cloudflare at all.
Deploying to Cloudflare Workers, and the local Docker path
Deployment is a first-class path in this project rather than an afterthought, and the README leads with a one-click button for Cloudflare Workers.
The build and deploy commands are in the package.json scripts:
npm run buildnpm run deployand the local development server is wrangler dev, which is the standard Cloudflare local runtime, with wrangler publish also present as a separate script. The build itself is vite build, and the package main points at worker/index.js, so the Worker entry point is at the root of the package rather than in a build directory.
The repository layout is correspondingly simple: client/ for the browser code, server/ for the Node server used by the Docker path, worker/ for the Cloudflare Worker, plus wrangler.toml for the Worker configuration and vite.config.js for the front end build. There is also a Dockerfile at the root, which builds the frontend with Vite in a Node 18 Alpine stage and then produces a minimal alpine:3.16 image with Node.js and nginx, where nginx serves the static client and the server directory is copied in with its production dependencies.
The Dockerfile's dependency handling is worth noting for anyone rebuilding it. The server stage copies only server/package.json before installing with --production --no-package-lock --no-audit, then copies just server.js, which keeps the layer cache useful and the image small. The front end stage runs npm ci --no-audit with a fallback to npm install --no-audit, and then npm run build:docker, which is the same vite build under a different script name.
The homepage is a deployed instance at nodecrypt.shuaiplus.workers.dev, so you can see the thing running before you deploy anything. The repository also has README_EN.md alongside the main README, and the default branch is main with the licence declared as ISC in both the package.json and the repository licence file.
What the project asks its users to accept
Two things in the README deserve to be stated plainly rather than paraphrased.
The first is the self-verification offer. The README says users can watch the full encrypt and decrypt process in the browser developer tools to verify that messages really are encrypted in transit, and that packet capture tools can be used to confirm that all WebSocket traffic is unreadable encrypted content. That is an unusual invitation, and it is the right one for a project whose credibility rests on a claim about what the server can and cannot see. It is also the kind of verification that a determined reader can repeat rather than take on trust.
The second is the disclaimer, and it is unusually clear. The project is for learning and technical research only, must not be used for any illegal or criminal activity, users should comply with the laws of their own country and region, the authors accept no legal responsibility for the use of the software, and it should be used in a lawful and compliant way.
On maintenance, the repository is not archived and the last push was on 2026-08-17, on the main branch, with no GitHub releases. The version in package.json is 1.0.0. So the code is being touched, but there is no tagged release history to point at and no changelog to consult before an upgrade, and the deploy is a wrangler deploy of whatever is on main.
The ISC licence is permissive and short, permitting use, copying, modification and distribution with attribution, and this review does not interpret licence terms.
The honest assessment is of a well-documented, self-hostable design that is explicit about its own limits. The security model is layered and the parts are named. The gap between the design and a production deployment is not in the cryptography, which is standard primitives, but in the surrounding engineering: a single group key derived per pair by hand, a room password shared out of band, an ephemeral RSA key that clients are told to trust on first connection, and pure JavaScript crypto in the path where the Web API might not be available. Those are the places to look before you rely on it for anything that matters.
Editorial conclusion
NodeCrypt fits a user who wants a small real-time chat with no accounts, no history and no server able to read the messages, and the deployment story is short because the whole backend is a Cloudflare Worker with Durable Objects. It is the wrong choice for a deployment where the cryptography must be audited to a higher standard than a browser app built on elliptic.js and aes-js, and the wrong choice for group chat, because the documented key exchange derives a separate Curve25519 key per pair rather than implementing a group key protocol. Before you rely on it, read the key derivation in the source, in particular the XOR of the ECDH shared secret with SHA256 of the room password, and treat the room password as the single point of failure the README says it is.
Frequently asked questions
How does NodeCrypt keep the server from reading messages?
The server acts as a blind relay. All encryption and decryption happen in the client, there is no persistent storage, and the message content is ChaCha20 encrypted with the room password folded into the derived key, with only the AES-256 transport layer readable by the server.
What role does the room password play in NodeCrypt's encryption?
The room password is a key derivation factor. The README gives the formula as the final shared key equals the ECDH shared secret XOR SHA256 of the room password, which isolates rooms with different passwords and keeps the password unknown to the server, but it also means a leaked password exposes that room's traffic.
How do I deploy NodeCrypt to Cloudflare Workers?
The README offers a one-click deploy button, and the package.json scripts provide the same path by hand: `npm run build` runs vite build and `npm run deploy` runs wrangler deploy. Local development uses `wrangler dev`.
Which libraries do the cryptography in NodeCrypt?
The Web Cryptography API for the browser's native implementation with hardware acceleration, elliptic.js for Curve25519 and P-384, aes-js as a pure JavaScript AES implementation, js-chacha20 for the ChaCha20 stream cipher and js-sha256 for hashing.
Is NodeCrypt still being updated and under what licence?
The repository is not archived and the last push was on 2026-08-17, with no GitHub releases and the package at version 1.0.0. The licence is ISC, declared in the package.json and in the LICENSE file at the root of the tree.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/shuaiplus-nodecrypt)Was this useful?