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.
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:
bashnode 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.
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:
bashnode dh_classic_demo.js
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.
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:
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.
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).
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.
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 }
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"
.
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?
Great question — “uniformly random” is a term from probability / cryptography, and it’s important in the Diffie–Hellman flow.
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.
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.
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)?
Step 2 (shared secret computation) in the first implementation is here:
jsconst 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.
Yes ✅
AES-GCM (AES in Galois/Counter Mode) gives you both:
Encryption (confidentiality) → it scrambles the plaintext into ciphertext.
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:
jsconst 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?
Exactly ✅ — with
AES-GCM you
don’t need a separate HMAC key.
One key handles both
encryption + authentication.
Salt makes the derived key unique and unpredictable, even if the shared secret repeats. It prevents key reuse and thwarts precomputed attacks.
An IV (Initialization Vector) is a random, one-time value that ensures each encryption with the same key produces different ciphertext.
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.
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:
Ephemeral key exchange (forward secrecy).
Key derivation (HKDF) to produce uniformly random symmetric keys.
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?
Perfect — here’s a concise, secure way to authenticate the servers while keeping the ECDH + AES-GCM flow:
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.
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 }
On the receiving server:
jsconst 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.
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?
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:
Encryption: TLS uses ephemeral key exchanges (ECDHE) + symmetric encryption (AES-GCM or ChaCha20-Poly1305).
Authentication: The server proves its identity via a trusted certificate.
Integrity: Messages can’t be modified without detection.
Forward secrecy: If ephemeral keys are used (ECDHE), past sessions stay secure even if long-term keys leak.
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?