Crypto recipe
Key exchange with X25519
Derive a shared secret over a public channel with X25519 Diffie-Hellman.
Key exchange lets two parties who have never met derive a shared secret over a public channel, so an eavesdropper learns nothing. X25519 (ECDH on Curve25519) is the modern default.
Two cautions. Never use the raw Diffie-Hellman output directly as an encryption key — run it through a KDF such as HKDF first. And plain X25519 has no authentication, so on its own it is open to a man-in-the-middle; pair it with signatures or a protocol like Noise or TLS in production.
Get it right
- Use X25519 for new designs; it sidesteps NIST-curve parameter pitfalls.
- Never use the raw shared secret as a key — derive with HKDF first.
- Authenticate the exchange (signatures, certificates) to stop man-in-the-middle.
- Use ephemeral keys for forward secrecy wherever the protocol allows.
Implementation
Setup Standard library (crypto/ecdh, Go 1.20+).
package main
import (
"crypto/ecdh"
"crypto/rand"
"fmt"
)
func main() {
curve := ecdh.X25519()
// Each party generates a key pair and publishes its public key.
alicePriv, _ := curve.GenerateKey(rand.Reader)
bobPriv, _ := curve.GenerateKey(rand.Reader)
// Each derives the same secret from its own private + the peer's public key.
aliceShared, _ := alicePriv.ECDH(bobPriv.PublicKey())
bobShared, _ := bobPriv.ECDH(alicePriv.PublicKey())
fmt.Println(string(aliceShared) == string(bobShared)) // true
// Do NOT use the raw secret as a key — run it through HKDF first.
} Setup pip install cryptography.
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
alice_private = X25519PrivateKey.generate()
bob_private = X25519PrivateKey.generate()
# Each side combines its private key with the peer's public key.
alice_shared = alice_private.exchange(bob_private.public_key())
bob_shared = bob_private.exchange(alice_private.public_key())
assert alice_shared == bob_shared
# Pass the shared secret through HKDF before using it as an encryption key. Setup Built in (node:crypto; diffieHellman since Node 13.9).
import { generateKeyPairSync, diffieHellman } from 'node:crypto';
const alice = generateKeyPairSync('x25519');
const bob = generateKeyPairSync('x25519');
// Each side combines its private key with the peer's public key.
const aliceShared = diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey });
const bobShared = diffieHellman({ privateKey: bob.privateKey, publicKey: alice.publicKey });
// aliceShared.equals(bobShared) === true
// Run the shared secret through HKDF before using it as a key. Setup dotnet add package NSec.Cryptography. .NET's built-in ECDiffieHellman covers NIST P-curves but not X25519.
using NSec.Cryptography;
var x25519 = KeyAgreementAlgorithm.X25519;
using Key alice = Key.Create(x25519);
using Key bob = Key.Create(x25519);
using SharedSecret aliceShared = x25519.Agree(alice, bob.PublicKey);
// Derive a real key from the shared secret with HKDF — don't use it directly.
byte[] key = KeyDerivationAlgorithm.HkdfSha256.DeriveBytes(
aliceShared, ReadOnlySpan<byte>.Empty, ReadOnlySpan<byte>.Empty, 32); Setup libsodium (crypto_kx wraps X25519 with a built-in KDF).
#include <sodium.h>
int main() {
if (sodium_init() < 0) return 1;
unsigned char client_pk[crypto_kx_PUBLICKEYBYTES], client_sk[crypto_kx_SECRETKEYBYTES];
unsigned char server_pk[crypto_kx_PUBLICKEYBYTES], server_sk[crypto_kx_SECRETKEYBYTES];
crypto_kx_keypair(client_pk, client_sk);
crypto_kx_keypair(server_pk, server_sk);
// crypto_kx derives two session keys (rx/tx) from the X25519 exchange via
// BLAKE2b — so there's no separate KDF step to remember.
unsigned char client_rx[crypto_kx_SESSIONKEYBYTES], client_tx[crypto_kx_SESSIONKEYBYTES];
crypto_kx_client_session_keys(client_rx, client_tx, client_pk, client_sk, server_pk);
unsigned char server_rx[crypto_kx_SESSIONKEYBYTES], server_tx[crypto_kx_SESSIONKEYBYTES];
crypto_kx_server_session_keys(server_rx, server_tx, server_pk, server_sk, client_pk);
// client_tx == server_rx and client_rx == server_tx
} Setup Built in since Java 11 (XDH, JEP 324).
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.crypto.KeyAgreement;
KeyPairGenerator kpg = KeyPairGenerator.getInstance("X25519");
KeyPair alice = kpg.generateKeyPair();
KeyPair bob = kpg.generateKeyPair();
KeyAgreement ka = KeyAgreement.getInstance("X25519");
ka.init(alice.getPrivate());
ka.doPhase(bob.getPublic(), true);
byte[] sharedSecret = ka.generateSecret();
// Feed sharedSecret into HKDF before using it as a key. Setup Cargo.toml: x25519-dalek = "2", rand = "0.8".
use rand::rngs::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey};
fn main() {
let alice_secret = EphemeralSecret::random_from_rng(OsRng);
let alice_public = PublicKey::from(&alice_secret);
let bob_secret = EphemeralSecret::random_from_rng(OsRng);
let bob_public = PublicKey::from(&bob_secret);
let alice_shared = alice_secret.diffie_hellman(&bob_public);
let bob_shared = bob_secret.diffie_hellman(&alice_public);
assert_eq!(alice_shared.as_bytes(), bob_shared.as_bytes());
// Run alice_shared.as_bytes() through HKDF before using it as a key.
}