X25519 Explained for Developers: Key Exchange Without the Magic
If you've ever seen "X25519" in a crypto library's dependency tree and moved on without asking what it actually does, this post is for you. You don't need a PhD to understand it — just the problem it solves and why this particular curve got chosen for almost everything.
The problem: two parties, one shared secret, no safe channel
Say Alice and Bob want to agree on a secret key, but they can only talk over a channel an attacker can observe (the internet, basically). This is the classic problem Diffie-Hellman (DH) solves: it lets two parties derive a shared secret without ever transmitting it directly.
Classic DH works over modular arithmetic with huge numbers. It works, but it's slow and the keys are large. Elliptic curve Diffie-Hellman (ECDH) gets the same mathematical property — a commutative key exchange — with much smaller keys and much faster operations.
Curve25519: the curve, not the algorithm
Curve25519 is a specific elliptic curve designed by Daniel J. Bernstein, built from the ground up to be fast and hard to implement incorrectly. Its key properties:
- Constant-time arithmetic is easier to get right (less surface for timing side-channel attacks).
- Resistant to invalid-curve attacks, a historical issue with other curves where an attacker could send points outside the expected curve.
- 32-byte keys, cheap to generate and operate on even on modest hardware.
X25519: the ECDH function over Curve25519
Here's the nuance a lot of people skip: Curve25519 is the curve; X25519 is the key-exchange (ECDH) function defined over it, specified in RFC 7748. It's the thing you actually call from your code.
The scheme, simplified:
- Each party generates a key pair: a private key (a random 32-byte scalar) and a public key (that scalar multiplied by a fixed base point on the curve).
- Alice sends her public key to Bob, and vice versa — over a channel that can be entirely public.
- Alice computes
secret = alicePrivate * bobPublic. Bob computessecret = bobPrivate * alicePublic. Because of the curve's properties, both land on the same value.
An observer who only sees the two public keys can't reconstruct that secret without solving the elliptic curve discrete logarithm problem, which is computationally infeasible at current key sizes.
In code, using the open primitives from @koove/crypto:
import { generateIdentityKeyPair } from '@koove/crypto';
const alice = generateIdentityKeyPair();
const bob = generateIdentityKeyPair();
// alice.publicKey and bob.publicKey can be shared openly
// alice.privateKey and bob.privateKey never leave the device
What X25519 does NOT give you: encryption
This part gets skipped a lot: X25519 only produces a shared secret — it doesn't encrypt anything on its own. That raw shared secret shouldn't be used directly as a symmetric key either; it needs to go through a key derivation function (HKDF-SHA256, in Koove's case) to get a properly-shaped key, which then gets used with an authenticated cipher like AES-256-GCM.
This pattern — X25519 to agree on / wrap a key, AES-256-GCM to encrypt the actual data — is called envelope encryption, and it's exactly what Koove uses to protect secrets:
import { generateIdentityKeyPair, encryptSecret, sealKey, openKey } from '@koove/crypto';
const bob = generateIdentityKeyPair();
// The data key (AES-256-GCM) encrypts the secret itself, once
const { envelope, dataKey } = encryptSecret('sk_live_...');
// That data key gets "sealed" (wrapped) with Bob's X25519 public key
const sealedForBob = sealKey(dataKey, bob.publicKey);
// Only Bob's device, holding the matching private key, can open it
const recoveredKey = openKey(sealedForBob, bob.privateKey);
Koove's server never sees dataKey in plaintext or private keys — only ciphertext and sealed envelopes. That's the difference between "encrypted in transit" and actual end-to-end encryption.
What X25519 also doesn't give you: automatic forward secrecy
If the X25519 keys you use are static (a fixed identity per device or user, which is the typical model for a secrets manager), you don't get forward secrecy by default: if that private key is ever compromised, an attacker could theoretically decrypt older envelopes sealed with the matching public key.
Protocols like Signal solve this with a ratchet — continuous rotation of ephemeral keys. Koove does not implement a ratchet — it uses X25519 + AES-256-GCM with per-device identities, which is the right design for "retrieve a secret when I need it," but it isn't equivalent in guarantees to a messaging protocol like Signal. Worth stating plainly: these are different properties for different use cases.
Why this matters in the AI-generated code era
When an AI assistant scaffolds an integration with Stripe or a third-party API, it tends to paste the key straight into code or an unencrypted .env file. Understanding X25519 isn't academic here — it's the foundation for why a secret can stay encrypted right up until the moment it's needed, decrypting only on a device or backend verified with App Attest / Play Integrity and biometrics.
In practice, for your day-to-day workflow, that looks like:
koove set STRIPE_SECRET_KEY sk_live_xxx --env prod
Your code only references STRIPE_SECRET_KEY by name. The actual value never passes through the AI assistant or lands in the repo.
Practical takeaways
- Don't implement ECDH from scratch. Use audited libraries (
libsodium,noble-curves, or already-packaged primitives like@koove/crypto, which are open source and auditable). - X25519 solves key agreement, not full encryption — you need a KDF plus an AEAD cipher around it.
- If your main threat model is "secrets leaked in AI-generated code," the right design is end-to-end encryption with device-verified identities, not Signal-style forward secrecy.
You can read the full architecture, including the design tradeoffs and their limits, in the trust and security center, or check the technical docs for the SDK and CLI at /docs/sdk. For specific threat-model questions, the FAQ covers the common ones.
Try Koove
If you're ready to stop pasting plaintext keys into your code and start using real end-to-end encryption from your first commit, create a free account and try koove set on your project in under five minutes.