X25519

Header: #include <cryptopp/xed25519.h> | Namespace: CryptoPP Since: Crypto++ 8.0

X25519 is a modern Diffie-Hellman key exchange algorithm built on Curve25519. It allows two parties to establish a shared secret over an insecure channel. It’s fast, simple, and resistant to many cryptographic attacks.

Quick Example

#include <cryptopp/xed25519.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hex.h>
#include <iostream>

int main() {
    using namespace CryptoPP;

    AutoSeededRandomPool rng;

    // Alice generates her key pair
    x25519 alice;
    SecByteBlock alicePrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock alicePublic(x25519::PUBLIC_KEYLENGTH);
    alice.GenerateKeyPair(rng, alicePrivate, alicePublic);

    // Bob generates his key pair
    x25519 bob;
    SecByteBlock bobPrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock bobPublic(x25519::PUBLIC_KEYLENGTH);
    bob.GenerateKeyPair(rng, bobPrivate, bobPublic);

    // Both compute the same shared secret
    SecByteBlock aliceShared(x25519::SHARED_KEYLENGTH);
    SecByteBlock bobShared(x25519::SHARED_KEYLENGTH);

    if (!alice.Agree(aliceShared, alicePrivate, bobPublic))
        throw std::runtime_error("Alice's key agreement failed");

    if (!bob.Agree(bobShared, bobPrivate, alicePublic))
        throw std::runtime_error("Bob's key agreement failed");

    // Verify both parties have the same shared secret
    if (std::memcmp(aliceShared, bobShared, x25519::SHARED_KEYLENGTH) == 0) {
        std::cout << "Key exchange successful!" << std::endl;
    }

    return 0;
}

Usage Guidelines

ℹ️

Do:

  • Use X25519 for modern key exchange (recommended over traditional DH)
  • Generate keys using AutoSeededRandomPool
  • Derive encryption keys from shared secret using HKDF
  • Verify that Agree() returns true (detects small-order attacks)
  • Use ephemeral keys for forward secrecy

Avoid:

  • Reusing private keys across multiple exchanges (weakens forward secrecy)
  • Using shared secret directly as encryption key (use HKDF instead)
  • Ignoring Agree() return value—small-order or invalid keys cause it to return false

Class: x25519

Elliptic curve Diffie-Hellman key exchange using Curve25519.

Constants

static const int SECRET_KEYLENGTH = 32;  // Private key size (256 bits)
static const int PUBLIC_KEYLENGTH = 32;  // Public key size (256 bits)
static const int SHARED_KEYLENGTH = 32;  // Shared secret size (256 bits)

Constructors

Default Constructor

x25519();

Create an empty X25519 object (for loading existing keys).

Example:

x25519 kex;
// Load keys later...

Constructor with RNG

x25519(RandomNumberGenerator& rng);

Create X25519 object and generate a new key pair.

Parameters:

  • rng - Random number generator (use AutoSeededRandomPool)

Example:

AutoSeededRandomPool rng;
x25519 kex(rng);  // Keys generated automatically

Constructor with Private Key

x25519(const byte x[SECRET_KEYLENGTH]);

Create X25519 object from existing private key. Public key is computed.

Parameters:

  • x - 32-byte private key

Example:

byte privateKey[32];
// ... load private key ...
x25519 kex(privateKey);  // Public key computed from private

Constructor with Both Keys

x25519(const byte y[PUBLIC_KEYLENGTH], const byte x[SECRET_KEYLENGTH]);

Create X25519 object from existing key pair.

Parameters:

  • y - 32-byte public key
  • x - 32-byte private key

Note: Public key is not validated in constructor.

Example:

byte publicKey[32], privateKey[32];
// ... load keys ...
x25519 kex(publicKey, privateKey);

Methods

GenerateKeyPair()

void GenerateKeyPair(RandomNumberGenerator& rng,
                     byte* privateKey, byte* publicKey) const;

Generate a new key pair.

Parameters:

  • rng - Random number generator
  • privateKey - Output buffer (32 bytes)
  • publicKey - Output buffer (32 bytes)

Example:

AutoSeededRandomPool rng;
byte privateKey[x25519::SECRET_KEYLENGTH];
byte publicKey[x25519::PUBLIC_KEYLENGTH];

x25519 kex;
kex.GenerateKeyPair(rng, privateKey, publicKey);

GeneratePrivateKey()

void GeneratePrivateKey(RandomNumberGenerator& rng,
                        byte* privateKey) const;

Generate a new private key only.

Parameters:

  • rng - Random number generator
  • privateKey - Output buffer (32 bytes)

Example:

AutoSeededRandomPool rng;
SecByteBlock privateKey(x25519::SECRET_KEYLENGTH);

x25519 kex;
kex.GeneratePrivateKey(rng, privateKey);

GeneratePublicKey()

void GeneratePublicKey(RandomNumberGenerator& rng,
                       const byte* privateKey,
                       byte* publicKey) const;

Derive public key from private key.

Parameters:

  • rng - Random number generator (unused but required by interface)
  • privateKey - 32-byte private key
  • publicKey - Output buffer (32 bytes)

Example:

AutoSeededRandomPool rng;
byte privateKey[32], publicKey[32];

x25519 kex;
kex.GeneratePrivateKey(rng, privateKey);
kex.GeneratePublicKey(rng, privateKey, publicKey);

Agree()

bool Agree(byte* agreedValue,
           const byte* privateKey,
           const byte* otherPublicKey,
           bool validateOtherPublicKey = true) const;

Perform key exchange to compute shared secret.

Parameters:

  • agreedValue - Output shared secret (32 bytes)
  • privateKey - Your private key (32 bytes)
  • otherPublicKey - Other party’s public key (32 bytes)
  • validateOtherPublicKey - Validate public key (default: true)

Returns: true if successful, false if validation fails (e.g., small-order or invalid public key)

Important: Always check return value. A false return indicates the peer’s public key failed validation.

Example:

SecByteBlock sharedSecret(x25519::SHARED_KEYLENGTH);

if (!kex.Agree(sharedSecret, myPrivate, theirPublic)) {
    std::cerr << "Key exchange failed - invalid public key!" << std::endl;
    return 1;
}

// Use sharedSecret with HKDF to derive encryption keys

ClampKey()

void ClampKey(byte x[SECRET_KEYLENGTH]) const;

Clamp a private key to meet X25519 requirements.

Parameters:

  • x - Private key to clamp (modified in-place)

Details: Sets specific bits: x[0] &= 248, x[31] &= 127, x[31] |= 64

Note: Keys generated by the library are already clamped.

Example:

byte privateKey[32];
// ... load raw key ...

x25519 kex;
kex.ClampKey(privateKey);  // Ensure key is properly formatted

IsClamped()

bool IsClamped(const byte x[SECRET_KEYLENGTH]) const;

Check if a private key is properly clamped.

Parameters:

  • x - Private key to check

Returns: true if key is clamped, false otherwise

IsSmallOrder()

bool IsSmallOrder(const byte y[PUBLIC_KEYLENGTH]) const;

Check if a public key has small order (security vulnerability).

Parameters:

  • y - Public key to check

Returns: true if key has small order (reject it!), false if safe

Note: Agree() validates automatically if validateOtherPublicKey = true.

Validate()

bool Validate(RandomNumberGenerator& rng, unsigned int level) const;

Validate the key pair.

Parameters:

  • rng - Random number generator
  • level - Validation level (0-3, higher is more thorough)

Returns: true if keys are valid

Complete Example: Secure Messaging

#include <cryptopp/xed25519.h>
#include <cryptopp/osrng.h>
#include <cryptopp/hkdf.h>
#include <cryptopp/sha.h>
#include <cryptopp/aes.h>
#include <cryptopp/gcm.h>
#include <cryptopp/secblock.h>
#include <iostream>

using namespace CryptoPP;

// Derive encryption and MAC keys from shared secret
void deriveKeys(const SecByteBlock& sharedSecret,
                SecByteBlock& encKey,
                SecByteBlock& macKey) {
    HKDF<SHA256> hkdf;

    // Info string to bind keys to application
    byte info[] = "SecureMessaging v1.0";

    // Derive 32-byte encryption key
    hkdf.DeriveKey(encKey, encKey.size(),
                   sharedSecret, sharedSecret.size(),
                   nullptr, 0,  // No salt
                   info, sizeof(info) - 1);

    // Derive 32-byte MAC key (different info)
    byte macInfo[] = "SecureMessaging v1.0 - MAC";
    hkdf.DeriveKey(macKey, macKey.size(),
                   sharedSecret, sharedSecret.size(),
                   nullptr, 0,
                   macInfo, sizeof(macInfo) - 1);
}

// Encrypt message using keys derived from X25519
std::string encryptMessage(const std::string& plaintext,
                           const SecByteBlock& encKey) {
    AutoSeededRandomPool rng;

    byte iv[12];
    rng.GenerateBlock(iv, sizeof(iv));

    std::string ciphertext;
    GCM<AES>::Encryption enc;
    enc.SetKeyWithIV(encKey, encKey.size(), iv, sizeof(iv));

    StringSource(plaintext, true,
        new AuthenticatedEncryptionFilter(enc,
            new StringSink(ciphertext)
        )
    );

    // Prepend IV to ciphertext
    std::string result;
    result.append((const char*)iv, sizeof(iv));
    result.append(ciphertext);

    return result;
}

int main() {
    AutoSeededRandomPool rng;

    // Alice generates ephemeral key pair
    x25519 alice(rng);
    SecByteBlock alicePrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock alicePublic(x25519::PUBLIC_KEYLENGTH);
    alice.GenerateKeyPair(rng, alicePrivate, alicePublic);

    // Bob generates ephemeral key pair
    x25519 bob(rng);
    SecByteBlock bobPrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock bobPublic(x25519::PUBLIC_KEYLENGTH);
    bob.GenerateKeyPair(rng, bobPrivate, bobPublic);

    // Both perform key exchange
    SecByteBlock aliceShared(x25519::SHARED_KEYLENGTH);
    SecByteBlock bobShared(x25519::SHARED_KEYLENGTH);

    if (!alice.Agree(aliceShared, alicePrivate, bobPublic)) {
        std::cerr << "Alice: Key exchange failed" << std::endl;
        return 1;
    }

    if (!bob.Agree(bobShared, bobPrivate, alicePublic)) {
        std::cerr << "Bob: Key exchange failed" << std::endl;
        return 1;
    }

    // Derive encryption keys from shared secret
    SecByteBlock encKey(32), macKey(32);
    deriveKeys(aliceShared, encKey, macKey);

    // Alice encrypts a message
    std::string plaintext = "Hello Bob, this is Alice!";
    std::string ciphertext = encryptMessage(plaintext, encKey);

    std::cout << "Message encrypted successfully" << std::endl;
    std::cout << "Ciphertext size: " << ciphertext.size() << " bytes" << std::endl;

    return 0;
}

Complete Example: Forward Secrecy

#include <cryptopp/xed25519.h>
#include <cryptopp/osrng.h>
#include <iostream>

using namespace CryptoPP;

// Simulate ephemeral key exchange (new keys each session)
void forwardSecretSession(int sessionNumber) {
    AutoSeededRandomPool rng;

    // Generate NEW ephemeral keys for THIS session
    x25519 alice(rng);
    x25519 bob(rng);

    SecByteBlock alicePrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock alicePublic(x25519::PUBLIC_KEYLENGTH);
    alice.GenerateKeyPair(rng, alicePrivate, alicePublic);

    SecByteBlock bobPrivate(x25519::SECRET_KEYLENGTH);
    SecByteBlock bobPublic(x25519::PUBLIC_KEYLENGTH);
    bob.GenerateKeyPair(rng, bobPrivate, bobPublic);

    // Perform key exchange
    SecByteBlock sharedSecret(x25519::SHARED_KEYLENGTH);
    alice.Agree(sharedSecret, alicePrivate, bobPublic);

    std::cout << "Session " << sessionNumber << ": Key exchange complete"
              << std::endl;

    // Use sharedSecret for encryption...
    // Keys are destroyed at end of scope (forward secrecy)
}

int main() {
    // Each session uses NEW ephemeral keys
    forwardSecretSession(1);
    forwardSecretSession(2);
    forwardSecretSession(3);

    // Past sessions cannot be decrypted even if current keys compromised
    std::cout << "Forward secrecy maintained across sessions" << std::endl;

    return 0;
}

Security

Security Properties

  • Security level: 128 bits (equivalent to AES-128)
  • Key size: 256 bits (32 bytes)
  • Shared secret size: 256 bits (32 bytes)
  • Algorithm: Elliptic Curve Diffie-Hellman on Curve25519
  • Standard: RFC 7748

Security Best Practices

  1. Always Validate Public Keys:

    // CORRECT - validates by default
    if (!kex.Agree(shared, myPrivate, theirPublic)) {
        // Reject - small-order attack detected
    }
    
    // WRONG - skips validation (vulnerable!)
    kex.Agree(shared, myPrivate, theirPublic, false);
  2. Use HKDF to Derive Keys:

    // WRONG - using shared secret directly
    AES::Encryption enc;
    enc.SetKey(sharedSecret, sharedSecret.size());
    
    // CORRECT - derive keys with HKDF
    HKDF<SHA256> hkdf;
    SecByteBlock key(32);
    hkdf.DeriveKey(key, key.size(), sharedSecret, ...);
  3. Forward Secrecy - Use Ephemeral Keys:

    // WRONG - reusing static keys
    x25519 static_kex(rng);  // Generated once
    // ... use for multiple sessions ...
    
    // CORRECT - new keys per session
    void newSession() {
        x25519 ephemeral_kex(rng);  // Fresh keys
        // ... perform key exchange ...
        // Keys destroyed at end of scope
    }
  4. Secure Key Storage:

    SecByteBlock privateKey(x25519::SECRET_KEYLENGTH);  // Auto-zeroing
    // NOT: byte privateKey[32];  // Leaves key in memory
    

Test Vectors (RFC 7748)

// Test Vector 1
byte alicePrivate[] = {
    0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d,
    0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2, 0x66, 0x45,
    0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a,
    0xb1, 0x77, 0xfb, 0xa5, 0x1d, 0xb9, 0x2c, 0x2a
};

byte bobPublic[] = {
    0xde, 0x9e, 0xdb, 0x7d, 0x7b, 0x7d, 0xc1, 0xb4,
    0xd3, 0x5b, 0x61, 0xc2, 0xec, 0xe4, 0x35, 0x37,
    0x3f, 0x83, 0x43, 0xc8, 0x5b, 0x78, 0x67, 0x4d,
    0xad, 0xfc, 0x7e, 0x14, 0x6f, 0x88, 0x2b, 0x4f
};

// Expected shared secret
byte expectedShared[] = {
    0x4a, 0x5d, 0x9d, 0x5b, 0xa4, 0xce, 0x2d, 0xe1,
    0x72, 0x8e, 0x3b, 0xf4, 0x80, 0x35, 0x0f, 0x25,
    0xe0, 0x7e, 0x21, 0xc9, 0x47, 0xd1, 0x9e, 0x33,
    0x76, 0xf0, 0x9b, 0x3c, 0x1e, 0x16, 0x17, 0x42
};

// Verify
x25519 kex;
byte sharedSecret[32];
bool ok = kex.Agree(sharedSecret, alicePrivate, bobPublic);
assert(ok);  // Should succeed with valid test vectors

assert(std::memcmp(sharedSecret, expectedShared, 32) == 0);

Performance

Benchmarks (Approximate)

OperationTime (µs)Notes
Key generation40-80Generate key pair
Key exchange40-80Compute shared secret
Public key derivation40-80Derive public from private

Platform: Modern x86-64 CPU, software implementation

Comparison with RSA

FeatureX25519RSA-2048
Key gen speed⚡⚡⚡⚡⚡ Very fast⚡ Slow
Key exchange speed⚡⚡⚡⚡⚡ Very fast⚡ Slow
Public key size32 bytes256+ bytes
Private key size32 bytes1024+ bytes
Security128-bit112-bit
Forward secrecy✅ Natural⚠️ Requires ephemeral

When to Use X25519

✅ Use X25519 for:

  1. Modern Key Exchange - Preferred over traditional DH/RSA key exchange
  2. Forward Secrecy - Ephemeral keys for each session
  3. VPN/TLS - Secure channel establishment (WireGuard, TLS 1.3)
  4. Secure Messaging - Signal, WhatsApp, etc.
  5. Any Diffie-Hellman Need - Faster and simpler than traditional DH

❌ Don’t use X25519 for:

  1. Digital Signatures - Use Ed25519 instead
  2. Encryption - X25519 is key exchange, not encryption
  3. Authentication - Use Ed25519 signatures instead

X25519 vs Alternatives

AlgorithmSpeedKey SizeSecurityUse Case
X25519⚡⚡⚡⚡⚡32 bytes128-bitKey exchange ⭐
Traditional DH⚡⚡256+ bytes112-bitLegacy
RSA key exchange256+ bytes112-bitLegacy
ECDH P-256⚡⚡⚡⚡64 bytes128-bitNIST required

Thread Safety

Thread-safe for read operations after construction. Key generation and agreement are safe to call from multiple threads if using separate objects.

// Safe - separate objects per thread
void threadFunc() {
    AutoSeededRandomPool rng;
    x25519 kex(rng);  // Thread-local
    // ... use kex ...
}

Exceptions

  • InvalidArgument - Invalid key length
  • InvalidDataFormat - Malformed key data

Algorithm Details

  • Curve: Curve25519 (Montgomery curve)
  • Field: Prime field with p = 2^255 - 19
  • Coordinate system: Montgomery coordinates
  • Cofactor: 8 (handled automatically)
  • Standard: RFC 7748
  • Implementation: Based on curve25519-donna by Andrew Moon

See Also