DEV Community

Cover image for Big Companies steal your data, so I made a secure chat for you.
kijmoshi
kijmoshi

Posted on

Big Companies steal your data, so I made a secure chat for you.

Every team has this moment. Someone shares something sensitive in Slack. Someone else says "we shouldn't use this for that." Then comes the mass migration to a different app — one with a privacy policy nobody reads, on servers nobody controls, with an encryption model nobody has verified.

Your options? Signal (great, but mobile-first and phone-number-tied). Telegram (not E2EE by default). Matrix/Element (self-hostable, but the setup is an afternoon project and the UI is a lot). Discord (no). A team email thread (no).

All of them are either closed-source, require a phone number, live on someone else's server, or need a UI that feels like a full-time job to operate.

I wanted something simpler:

$ zypher work alice
Enter fullscreen mode Exit fullscreen mode

That's it. Terminal. Encrypted. Your server or mine.

That's Zypher.


The 30-Second Version

Install it:

npm i -g zypher-chat
Enter fullscreen mode Exit fullscreen mode

Spin up a server — one command clones, configures, and daemonizes it:

zypher server quickstart
Enter fullscreen mode Exit fullscreen mode

Register on it:

zypher new
Enter fullscreen mode Exit fullscreen mode

Open a conversation:

$ zypher work alice

  ╭─ alice ──────────────────────────────────╮
  │  alice  🟢 online                        │
  ╰──────────────────────────────────────────╯

  you › hey
  alice › hey! how's the project going?
  you › encrypting everything, obviously
Enter fullscreen mode Exit fullscreen mode

No phone number. No app store. No browser extension. Just a terminal.


Why Not Just Use Signal / Telegram / Matrix?

You can. But:

  • Signal needs a phone number. You can't register without a real mobile number. If your phone dies or you change numbers, you lose access. For a dev tool or internal team chat, that's an unnecessary external dependency.
  • Matrix/Element is powerful but heavy. Spinning up a Synapse server is a real infrastructure project. The client is feature-rich but not CLI-native.
  • Discord is a product, not a protocol. Not self-hostable, not open-source, not private.

  • It lives on someone else's server. All of these options require trusting a third party with your data. Even if the app is open-source, the deployment isn't. Zypher is self-hosted by design — you control the infrastructure and the data.

Zypher is for when you want a terminal, a server you control, and actual end-to-end encryption — not a SaaS product that happens to call itself private.


How It Actually Works

Registration and Key Generation

When you register on a server, Zypher generates two keypairs locally — before anything touches the network:

  • An Ed25519 identity key — your permanent signing key. Every message you send is signed with it. Recipients verify the signature. The server cannot forge a message from you without your private key.
  • An X25519 pre-key — a Diffie-Hellman key for key exchange. Other clients use it to compute a shared secret without ever transmitting one.

The private keys never leave your machine. They're stored in ~/.zypher/config.json, mode 0600. The server only receives your public keys.

DM Encryption — per-message ephemeral ratchet

When you send a DM to Alice:

  1. Zypher generates a fresh ephemeral X25519 keypair for this message only.
  2. It performs ECDH between your ephemeral private key and Alice's public pre-key, then runs the result through HKDF-SHA256 to derive a 256-bit AES key.
  3. The message is encrypted with AES-256-GCM using a random IV.
  4. The ephemeral public key (the "ratchet key") ships alongside the ciphertext so Alice can reproduce the same ECDH and decrypt.

The ratchet key is unique per message. Every message gets a fresh keypair. The server sees only ciphertext, nonces, and public keys — never plaintext. Even if a session key were ever compromised, past messages stay safe.

Group Encryption — shared key, individually wrapped

Groups use a shared symmetric key, but that key is never sent in the clear. When you create a group or invite someone:

  1. A random 256-bit group key is generated on your machine.
  2. For each member, that group key is encrypted separately using their X25519 pre-key (same ECDH → AES-256-GCM flow as DMs).
  3. Each member receives their own encrypted bundle. The server stores these bundles but cannot read the group key inside any of them.

Group messages are then encrypted with the shared group key + AES-256-GCM. Admin operations — kick, promote, rename — trigger a re-key: a new group key is generated and re-encrypted for each remaining member.

What the Server Actually Stores

The server is a dumb relay with a mailbox. It stores:

  • Usernames, bcrypt password hashes, and public keys (identity + pre-key)
  • Encrypted ciphertext for offline delivery — it cannot read any of it
  • Group membership and role metadata

It never sees plaintext. It never sees private keys. It never stores session keys.

Client A                    Server                  Client B
   │                           │                         │
   │── ECDH(ephemeral,         │                         │
   │      B's pre-key)         │                         │
   │── AES-256-GCM(msg) ──────▶│── store ciphertext ────▶│
   │                           │                         │── ECDH(my pre-key,
   │                           │                         │      ratchet_key)
   │                           │                         │── AES-256-GCM decrypt
Enter fullscreen mode Exit fullscreen mode

Things I Deliberately Didn't Build

  • No phone numbers. Registration is username + password. Nothing else required.
  • No centralized server. zypher server quickstart clones the server repo, configures it, and runs it as a local daemon. You own the infrastructure.
  • No cloud accounts. There's no "Zypher cloud." Every deployment is independent.
  • No browser client. It's a CLI. The terminal is the UI. If you want a GUI chat app, you're looking for Signal.
  • No multi-device key sync. Keys live on the device that generated them. Offline messages are queued on the server and delivered once — then gone from the queue.
  • No metadata minimization theater. The server knows who is messaging whom (routing requires it). What it doesn't know is what they're saying.

Ghost Mode

Type /ghost in any chat session:

  !! GHOST MODE !!  This erases everything.

  Type GHOST to confirm: █
Enter fullscreen mode Exit fullscreen mode

Confirm with the word GHOST, and Zypher deletes ~/.zypher/ entirely — config, private keys, account data, everything. The process exits immediately. Nothing remains on disk.

One command, scorched-earth wipe. Useful when you need it.


Offline Messaging

If Alice is offline when you send her a message, the server queues the encrypted ciphertext. When Alice reconnects, all queued messages are delivered in order and the queue is cleared. The server holds them only as long as it has to — it cannot read them, and they're deleted once delivered.


The Stack

The server is ~600 lines of JavaScript:

Component What it does
express + helmet HTTP API with security headers and rate limiting
socket.io Real-time message delivery
node:sqlite User accounts, offline queues, group metadata
bcrypt Password hashing
jsonwebtoken Session authentication

The client is a single index.js (~1,600 lines). No UI framework. One runtime dependency: socket.io-client. All crypto — X25519, Ed25519, AES-256-GCM, HKDF — is Node.js built-in (node:crypto). Nothing custom, nothing exotic.


Try It

# Install
npm i -g zypher-chat

# Quickstart: clones, configures, and launches a local server as a daemon
zypher server quickstart

# Register on it
zypher new

# Open a chat
zypher <servername> <username>
Enter fullscreen mode Exit fullscreen mode

Requires Node.js 22+. Works on Linux, macOS, and Windows.

Source code: github.com/real-kijmoshi/zypher

npm: npmjs.com/package/zypher-chat

Website: zypher.kijmoshi.xyz


Top comments (0)