building a lightweight chat app with Node.js and Socket.io—nothing crazy, just something to pass messages in real-time. Then came the “fun” idea: why not add end-to-end encryption?

Honestly, it sounded terrifying at first. But after a lot of trial, error, and late-night Googling, I got it working—and guess what? You can too.
Why Even Bother?
Yes, HTTPS is good. But once a message hits your server, it’s readable unless you encrypt it before sending.
End-to-end encryption (E2EE) means:
- The message is encrypted before it leaves the sender’s browser.
- Only the recipient can decrypt it using their private key.
- Even your own server is clueless about what’s inside.
Privacy win.
What I Used
- Node.js — backend
- Socket.io — real-time communication
- Crypto (Node’s built-in module) — for key generation and encryption
- HTML + vanilla JS — front-end
- Google — the real MVP
Read more about tech blogs.
Step 1: Generating RSA Key Pairs
Each user needs a public/private key pair. Public keys get shared. Private keys? Treat them like your Netflix password: private and sacred.
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
console.log("Public:", publicKey);
console.log("Private:", privateKey);
Note: I temporarily stored the private key in localStorage
(not ideal—just for demo).
Step 2: Encrypting Messages with the Receiver’s Public Key
Before sending a message, the sender encrypts it using the receiver’s public key.
const { publicEncrypt } = require('crypto');
function encrypt(publicKey, message) {
const buffer = Buffer.from(message, 'utf8');
const encrypted = publicEncrypt(publicKey, buffer);
return encrypted.toString('base64');
}
Simple, right? The real challenge is ensuring you have the correct key for the intended receiver.
Step 3: Decrypting Messages with the Receiver’s Private Key
On the receiving end, we decrypt using the recipient’s private key:
const { privateDecrypt } = require('crypto');
function decrypt(privateKey, encryptedData) {
const buffer = Buffer.from(encryptedData, 'base64');
const decrypted = privateDecrypt(privateKey, buffer);
return decrypted.toString('utf8');
}
Voilà. Alice encrypts with Bob’s public key, and only Bob’s private key can unlock it.
The Server’s Role? Just Forward Stuff
The chat server acts like a clueless postman. No peeking allowed.
const io = require('socket.io')(3000, {
cors: { origin: "*" }
});
io.on('connection', socket => {
socket.on('send-message', ({ to, data }) => {
socket.to(to).emit('receive-message', data);
});
});
Because the message is already encrypted, the server doesn’t (and can’t) understand it.
A Few Gotchas
- Key Safety: If your private key leaks, you’re toast. Store it securely (not
localStorage
in production). - Key Exchange: Be careful with public key sharing. Verify it—QR codes or hashes can help.
- Message Size Limits: RSA is not ideal for large data. Use RSA to securely share a symmetric AES key, then encrypt actual messages with AES.
Want to work on real project please visit Internboot.com
What I’d Do Differently Next Time
Use ECDH (Elliptic Curve Diffie-Hellman) instead of RSA—lighter and faster.
- Move encryption to the front-end completely.
- Add key verification using fingerprints or hashes.
But for a working demo? This setup delivers.
Final Thoughts: E2EE Isn’t Magic, It’s Math
Once you understand public/private keys, implementing encryption is less about wizardry and more about wiring. Is it perfect? Nope. But it works—and that’s a start.
If you’re building your own version, don’t stop at the basics. Explore:
- Perfect Forward Secrecy
- Hybrid encryption
- Decentralized key servers
- Password-based key derivation (PBKDF2 or scrypt)
Want the full source code? Let me know—I’ll drop it on GitHub or send it your way.