Diffie-Hellman in Node.js

how to implement Diffie hillman algorithm in node js?

Here’s a clean, modern way to do Diffie–Hellman in Node.js using only the built-in crypto module. I’ll show (A) a recommended ECDH (Curve25519 / X25519) version, and (B) the classic modular DH version. Both snippets simulate “Alice” and “Bob” in one file so you can run them directly.


A) ECDH (recommended): X25519 + HKDF + AES-GCM

js
// ecdh_x25519_demo.js const crypto = require('crypto'); // ---- 1) Each side generates an ECDH key pair (X25519) ---- const alice = crypto.createECDH('x25519'); alice.generateKeys(); // private kept internally, public is sent to Bob const bob = crypto.createECDH('x25519'); bob.generateKeys(); // Public keys to exchange (send over the wire, e.g., hex or base64) const alicePub = alice.getPublicKey(); // <Buffer ...> const bobPub = bob.getPublicKey(); // ---- 2) Each side computes the same shared secret ---- const aliceSecret = alice.computeSecret(bobPub); // <Buffer ...> const bobSecret = bob.computeSecret(alicePub); // These must match: console.log('Shared secrets equal?', aliceSecret.equals(bobSecret)); // ---- 3) Derive an encryption key from the raw secret (HKDF-SHA256) ---- // (Use a fresh random salt for each session and transmit it alongside the ciphertext.) const salt = crypto.randomBytes(16); const info = Buffer.from('session key'); // context string; optional but recommended const key = crypto.hkdfSync('sha256', salt, aliceSecret, info, 32); // 32B = AES-256 key // ---- 4) Use the derived key with an AEAD cipher (AES-256-GCM) ---- const iv = crypto.randomBytes(12); // 96-bit nonce for GCM const plaintext = Buffer.from('hello from alice'); // Encrypt (Alice) const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag(); // Transmit {salt, iv, ciphertext, tag, alicePub} to Bob. // Bob independently derives the same key using his secret and the same salt/info. // Decrypt (Bob) const bobKey = crypto.hkdfSync('sha256', salt, bobSecret, info, 32); const decipher = crypto.createDecipheriv('aes-256-gcm', bobKey, iv); decipher.setAuthTag(tag); const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); console.log('Decrypted:', decrypted.toString());

Run it:

bash
node ecdh_x25519_demo.js

Notes:

  • X25519 is fast and safer to use correctly than classic DH.

  • Always authenticate the peer’s public key (e.g., with signatures, certificates, or a PAKE) to prevent man-in-the-middle.

  • Rotate salts/IVs per session/message. Never reuse an (key, iv) pair with GCM.


B) Classic Diffie–Hellman (modp group) with built-in primes

js
// dh_classic_demo.js const crypto = require('crypto'); // Use a safe, standardized prime (e.g., 2048-bit) instead of ad-hoc small primes. const dhAlice = crypto.createDiffieHellman(2048); // Node picks a safe prime & generator dhAlice.generateKeys(); const dhBob = crypto.createDiffieHellman(dhAlice.getPrime(), dhAlice.getGenerator()); dhBob.generateKeys(); // Exchange public keys const alicePub = dhAlice.getPublicKey(); const bobPub = dhBob.getPublicKey(); // Compute shared secret const aliceSecret = dhAlice.computeSecret(bobPub); const bobSecret = dhBob.computeSecret(alicePub); console.log('Shared secrets equal?', aliceSecret.equals(bobSecret)); // Derive an AES key (HKDF-SHA256) const salt = crypto.randomBytes(16); const key = crypto.hkdfSync('sha256', salt, aliceSecret, Buffer.from('dh classic'), 32); // Encrypt something with AES-256-GCM const iv = crypto.randomBytes(12); const msg = Buffer.from('hello from classic dh'); const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const ct = Buffer.concat([cipher.update(msg), cipher.final()]); const tag = cipher.getAuthTag(); // Decrypt const keyBob = crypto.hkdfSync('sha256', salt, bobSecret, Buffer.from('dh classic'), 32); const dec = crypto.createDecipheriv('aes-256-gcm', keyBob, iv); dec.setAuthTag(tag); const pt = Buffer.concat([dec.update(ct), dec.final()]); console.log('Decrypted:', pt.toString());

Run it:

bash
node dh_classic_demo.js

Practical tips

  • Key formats: To send public keys over the wire, encode them (e.g., alice.getPublicKey('base64')). Decode on receipt.

  • Auth: DH/ECDH alone does not authenticate peers. Use digital signatures (Ed25519/ECDSA), certificates (mTLS), or a handshake protocol (e.g., TLS, Noise).

  • Replay/nonce safety: Never reuse an IV for GCM with the same key. Consider including message counters in your “info” or AAD.

  • KDF: crypto.hkdfSync requires modern Node (v16+). If you’re stuck on older Node, use a reputable HKDF implementation or upgrade.

If you want, I can adapt this into a small client/server demo that performs the key exchange over TCP/WebSocket and signs the public keys to prevent MITM.

explain the flow of the first impl

Got it 👍 — let’s walk through the flow of the first implementation (ECDH with X25519, HKDF, AES-GCM) step by step as if Alice and Bob are two parties talking over a network:


1. Key Pair Generation

  • Alice creates an X25519 ECDH key pair:

    • A private key (kept secret).

    • A public key (to send to Bob).

  • Bob does the same.

👉 At this point, Alice knows her private key + public key, Bob knows his.
They exchange only the public keys.


2. Shared Secret Computation

  • Alice takes Bob’s public key and combines it with her own private key via the ECDH math.

  • Bob takes Alice’s public key and combines it with his own private key.

👉 Both end up with the same raw shared secret.
This number is not directly usable as an encryption key (it might not be uniformly random).


3. Key Derivation (HKDF)

  • Both sides run the shared secret through HKDF (HMAC-based Key Derivation Function) with:

    • A random salt (sent along with ciphertext).

    • Some context info (like "session key").

    • Desired output length (32 bytes → AES-256 key).

👉 This produces a clean, uniformly random symmetric encryption key that both sides share.


4. Encryption (Alice → Bob)

  • Alice wants to send a message "hello from alice".

  • She generates a random IV (nonce) for AES-GCM.

  • Using the derived symmetric key + IV:

    • She encrypts the plaintext.

    • AES-GCM also produces an authentication tag, which protects integrity.

👉 She sends to Bob:

{ salt, iv, ciphertext, tag, alicePublicKey }

5. Decryption (Bob)

  • Bob receives the message bundle.

  • He recomputes the shared secret (using Alice’s public key + his private key).

  • He runs HKDF with the same salt/info to derive the same session key.

  • He decrypts the ciphertext using AES-GCM with the key + IV + tag.

👉 The authentication tag ensures the ciphertext hasn’t been tampered with.
Bob recovers "hello from alice".


🔑 Big Picture

  • ECDH provides a shared secret → both sides independently derive the same symmetric key.

  • HKDF ensures the key is safe, uniform, and session-unique.

  • AES-GCM ensures confidentiality + integrity for messages.

  • Only public keys + ciphertext travel over the network — private keys and shared secrets never leave the devices.


Would you like me to also draw a message flow diagram (Alice → Bob arrows showing which values are exchanged) so it’s crystal clear how the exchange works?

explain this uniformly random

Great question — “uniformly random” is a term from probability / cryptography, and it’s important in the Diffie–Hellman flow.


🔹 What “uniformly random” means

A value is uniformly random if:

  • Every possible outcome is equally likely.

  • There are no “biases” or “hot spots” where some values appear more often than others.

👉 Example:

  • Rolling a fair 6-sided die is uniform (each number 1–6 has probability 1/6).

  • Rolling a loaded die that lands on “6” more often is not uniform.


🔹 Why it matters in cryptography

Encryption keys must be indistinguishable from truly random values:

  • If some keys are more likely than others, attackers can guess keys faster (reduce brute-force cost).

  • “Uniformly random” keys make brute force no easier than trying every possible key.


🔹 Diffie–Hellman context

When Alice and Bob compute the raw ECDH shared secret:

  • The math produces a large integer (or buffer of bytes).

  • Its distribution is not guaranteed to be uniform across the whole space of possible values (some byte patterns may appear more often).

That’s why we don’t directly use the raw secret as an AES key.
Instead, we pass it through a Key Derivation Function (HKDF):

  • HKDF applies a cryptographic hash (like SHA-256) with salt.

  • This “extracts” and “expands” the raw secret into a uniformly random-looking key of the exact length we want (e.g., 32 bytes for AES-256).

  • Result: the derived key is safe to use and looks like it was picked by a perfect random generator.


Summary:
“Uniformly random” = every possible key is equally likely.
In Diffie–Hellman, the raw shared secret isn’t uniformly random, so we “smooth it out” with HKDF to get a secure, uniformly random key.


Do you want me to show you a small Node.js snippet that demonstrates the difference between a “raw DH secret distribution” vs. the “HKDF-derived key distribution” (like plotting histograms)?

where is step 2 in the code keep it short

Step 2 (shared secret computation) in the first implementation is here:

js
const aliceSecret = alice.computeSecret(bobPub); // Alice computes shared secret const bobSecret = bob.computeSecret(alicePub); // Bob computes shared secret

👉 That’s where both sides independently compute the same shared secret using their private key + the other’s public key.

Does aes-gcm handle both enc and auth as well?

Yes ✅

AES-GCM (AES in Galois/Counter Mode) gives you both:

  1. Encryption (confidentiality) → it scrambles the plaintext into ciphertext.

  2. Authentication (integrity + authenticity) → it produces an authentication tag that ensures:

    • The ciphertext hasn’t been modified.

    • The key + IV are correct.

    • Optional “associated data” (AAD, e.g. headers) also hasn’t been tampered with.

When decrypting, AES-GCM will only return the plaintext if the auth tag matches. If anything was altered, decryption fails with an error.

👉 That’s why in the code you saw:

js
const tag = cipher.getAuthTag(); // generated during encryption decipher.setAuthTag(tag); // required for decryption

So, AES-GCM = encryption + built-in MAC (auth) in one algorithm — no need to add HMAC separately.

Want me to also show you how to include Associated Data (AAD) in AES-GCM so you can authenticate extra info (like message headers) without encrypting it?

keep it short - So I don't another key for HMAC auth?

Exactly ✅ — with AES-GCM you don’t need a separate HMAC key.
One key handles both encryption + authentication.

keep it short - what is the usage of salt

Salt makes the derived key unique and unpredictable, even if the shared secret repeats. It prevents key reuse and thwarts precomputed attacks.

what is iv keep it short

An IV (Initialization Vector) is a random, one-time value that ensures each encryption with the same key produces different ciphertext.

explain this? const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag();

That snippet is the AES-GCM encryption step:

  • crypto.createCipheriv('aes-256-gcm', key, iv) → creates an AES-256-GCM cipher using the derived key and random IV.

  • cipher.update(plaintext) + cipher.final() → encrypts the plaintext into ciphertext.

  • cipher.getAuthTag() → retrieves the authentication tag (integrity check).

👉 Result = { ciphertext, tag }, which Bob will need (plus the IV) to decrypt and verify.

is this an industry standard for communication between two servers?

Yes, the pattern you’re using—ECDH (or DH) for key exchange + HKDF to derive a symmetric key + AES-GCM for encryption/authentication—is industry standard for secure server-to-server communication.

  • Variants of this flow are used in TLS 1.3, Signal protocol, and WireGuard.

  • Key points that make it standard:

    1. Ephemeral key exchange (forward secrecy).

    2. Key derivation (HKDF) to produce uniformly random symmetric keys.

    3. Authenticated encryption (AES-GCM or ChaCha20-Poly1305) to ensure confidentiality and integrity.

⚠️ Caveat: Real-world systems also authenticate the peers (certificates, signatures) to prevent MITM attacks. Your snippet only does key exchange + encryption; it doesn’t verify identities yet.

You want me to show a secure way to add authentication between the servers?

yes

Perfect — here’s a concise, secure way to authenticate the servers while keeping the ECDH + AES-GCM flow:


1. Use Long-Term Signing Keys

Each server has a long-term Ed25519 (or ECDSA) key pair:

  • Private key → kept secret.

  • Public key → distributed to the other server or via a trusted CA.


2. Sign the ECDH Public Key

When a server sends its ephemeral ECDH public key:

js
// server side const signature = crypto.sign(null, ecdhPubKey, signingPrivateKey);
  • ecdhPubKey = ephemeral X25519 public key.

  • signingPrivateKey = server’s long-term private key.

Send:

json
{ "pubKey": ecdhPubKey, "signature": signature }

3. Verify the Signature

On the receiving server:

js
const valid = crypto.verify( null, receivedPubKey, signingPublicKey, // sender's known long-term public key receivedSignature ); if (!valid) throw new Error("Invalid sender!");
  • This ensures the ephemeral key really comes from the trusted server.

  • Prevents Man-in-the-Middle (MITM) attacks.


4. Continue as Before

  • Compute the shared secret via computeSecret().

  • Derive symmetric key with HKDF.

  • Encrypt/decrypt messages with AES-GCM.


Result:

  • ECDH → forward secrecy.

  • AES-GCM → confidentiality + integrity.

  • Signature → authentication (verifies the server identity).


If you want, I can rewrite your Node.js ECDH + AES-GCM code to include this signing step so it’s fully secure for server-to-server communication.

Do you want me to do that?

What if the server is behind https?

If your server is already behind HTTPS, a lot of the security you’d get from ECDH + AES-GCM + signing is already handled by TLS:


HTTPS/TLS already provides:

  1. Encryption: TLS uses ephemeral key exchanges (ECDHE) + symmetric encryption (AES-GCM or ChaCha20-Poly1305).

  2. Authentication: The server proves its identity via a trusted certificate.

  3. Integrity: Messages can’t be modified without detection.

  4. Forward secrecy: If ephemeral keys are used (ECDHE), past sessions stay secure even if long-term keys leak.


Implications:

  • You don’t need to implement ECDH + AES-GCM manually over HTTPS; TLS already does it securely.

  • Manual key exchange is usually only necessary for:

    • Custom protocols not using TLS.

    • End-to-end encryption where TLS terminates at a proxy and you need encryption all the way to the server.

    • Mutual authentication with additional keys beyond HTTPS.


Summary:

  • For standard server-to-server communication over HTTPS, TLS handles encryption, integrity, and authentication.

  • Implementing your own ECDH/AES-GCM inside HTTPS is usually redundant unless you have very specific security requirements.

If you want, I can explain a scenario where you would still use ECDH + AES-GCM on top of HTTPS and why. Do you want me to?