js

How to Build End-to-End Encryption in Node.js with AES-GCM and RSA

Learn how to build end-to-end encryption in Node.js using AES-GCM and RSA to protect user data in transit. Start securing apps today.

How to Build End-to-End Encryption in Node.js with AES-GCM and RSA

Here’s a piece of code I wrote last year. It sends user data across the internet. Looking at it now, a cold worry settled in my stomach. What if someone intercepted that request? What if the database was breached? The data was just… sitting there. This wasn’t a theoretical problem; it was a flaw in something I built. This moment shifted my thinking from “how do I move data?” to “how do I protect it?” That’s why we’re talking about end-to-end encryption today. Not as a complex academic concept, but as a practical shield you can build. Ready to stop hoping data is safe and start knowing it is?

Think of sending a secret letter. You could put it in a locked box, but then you’d have to sneak the key to your friend. End-to-end encryption solves that key-sneaking problem. How? It uses two types of locks: a fast, strong one for the message itself, and a separate, clever system to securely send the key for that first lock. The message is shielded from the moment it leaves your hands until the intended person opens it. Even if someone intercepts the box, they see only a jumble of nonsense.

So, how do we build this in Node.js? We use its built-in tools—no extra libraries needed. We’ll create two main pieces: a system to seal the message (symmetric encryption) and a system to safely deliver the key (asymmetric encryption). Ever wonder why we need both? Why not just one super lock?

Let’s start by making the digital equivalent of a unique, single-use lock and key for each message. This is symmetric encryption, where the same key locks and unlocks. We’ll use a modern, reliable algorithm called AES in GCM mode. It’s fast for data of any size and has a built-in seal to detect tampering. Here is how you generate that random key and a crucial starting value (called an Initialization Vector) for each message.

// Generate a random key and IV for AES-256-GCM
const crypto = require('crypto');

async function generateMessageKey() {
    // A 256-bit (32-byte) cryptographically random key
    const key = crypto.randomBytes(32);
    // A 96-bit (12-byte) random Initialization Vector - crucial for security
    const iv = crypto.randomBytes(12);
    return { key, iv };
}

Now we have a strong key. But if we need to send this key to our friend to unlock the message, we have a problem. Sending it plainly defeats the whole purpose. This is the classic “key exchange” problem. How do you secretly deliver a secret?

This is where the second lock comes in: asymmetric encryption, specifically RSA. In this system, you have a key pair: a public key that can lock things, and a separate private key that can unlock them. You can give your public key to anyone, even post it online. They use it to lock (or “encrypt”) the AES key. Once locked with the public key, only your private key can open it. Let’s see how to create this powerful pair.

// Generate an RSA key pair for key wrapping
const { generateKeyPair } = require('crypto');
const { promisify } = require('util');
const generateKeyPairAsync = promisify(generateKeyPair);

async function createKeyPair() {
    const { publicKey, privateKey } = await generateKeyPairAsync('rsa', {
        modulusLength: 2048, // Strong enough for most uses
        publicKeyEncoding: { type: 'spki', format: 'pem' },
        privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
    });
    return { publicKey, privateKey };
}

See the elegance? Your friend takes your public key, uses it to lock the random AES key, and sends you the locked box. Only your private key, which never leaves your device, can open it. Now you have the AES key to read the main message. This combination—fast AES for the bulk data and secure RSA for the key—is the engine of modern private communication.

Let’s put it all together in a practical example. Imagine a function that takes a message and a recipient’s public key, and performs the complete sealing process. What do you think the final output needs to contain to be useful?

async function encryptMessage(plaintext, recipientPublicKey) {
    // 1. Generate a random AES key and IV for this message
    const { key: aesKey, iv } = await generateMessageKey();

    // 2. Encrypt the message itself with AES-GCM
    const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
    let encrypted = cipher.update(plaintext, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    const authTag = cipher.getAuthTag().toString('base64'); // Integrity check

    // 3. Encrypt (wrap) the AES key with the recipient's RSA public key
    const wrappedKey = crypto.publicEncrypt(
        {
            key: recipientPublicKey,
            padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
            oaepHash: 'sha256'
        },
        aesKey // The AES key to wrap
    );

    // The package includes everything needed for decryption
    return {
        encryptedMessage: encrypted,
        wrappedKey: wrappedKey.toString('base64'),
        iv: iv.toString('base64'),
        authTag: authTag
    };
}

On the other side, decryption is the reverse. You use your private RSA key to unwrap the AES key, then use that AES key with the IV and authentication tag to recover the original text. The process validates itself; if the tag doesn’t match, someone tampered with the message.

The beauty of this approach is in its boundaries. In a typical app, your server would help route these encrypted packages between users, but it would only ever handle the scrambled, wrapped data. It facilitates the connection without possessing the means to listen in. The power stays with the people in the conversation.

Building this changes your perspective as a developer. You start to see data not just as information, but as a responsibility. Implementing even a basic version of this pattern is a powerful step toward building systems that respect user privacy by design. It moves security from an abstract feature to a concrete line of code you write. What will you build that deserves this level of care?

If this breakdown of practical encryption helps you, please share it with a fellow developer. Have you tried implementing something similar? What challenges did you face? Let me know in the comments.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: end-to-end encryption, Node.js security, AES-GCM, RSA encryption, data protection



Similar Posts
Blog Image
Build Production-Ready GraphQL APIs with Apollo Server, TypeScript, and Redis Caching Tutorial

Build production-ready GraphQL APIs with Apollo Server 4, TypeScript, Prisma ORM & Redis caching. Master scalable architecture, authentication & performance optimization.

Blog Image
Complete Guide to Next.js Prisma ORM Integration: Build Type-Safe Full-Stack Apps Fast

Learn how to integrate Next.js with Prisma ORM for full-stack TypeScript development. Build type-safe apps with seamless database operations and API routes.

Blog Image
How to Build a Secure and Scalable API Gateway with Express and Kong

Learn to combine Express and Kong to create a powerful, secure API gateway that simplifies authentication, routing, and rate limiting.

Blog Image
Build Production-Ready GraphQL APIs: NestJS, Prisma, and Redis Caching Complete Guide

Build production-ready GraphQL APIs with NestJS, Prisma & Redis caching. Learn authentication, performance optimization & deployment best practices.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, high-performance web applications. Complete guide with setup, migration tips & best practices.

Blog Image
Build High-Performance GraphQL APIs with NestJS, Prisma, and Redis Caching Tutorial

Learn to build scalable GraphQL APIs with NestJS, Prisma, and Redis. Master database optimization, caching strategies, real-time subscriptions, and performance monitoring. Boost your API development skills today!