ECDH (Elliptic Curve Diffie-Hellman)
Efficient key exchange with elliptic curves
ECDH (Elliptic Curve Diffie-Hellman) is the elliptic curve variant of the Diffie-Hellman key exchange. It provides the same security guarantees as traditional DH but with much smaller key sizes, making it ideal for mobile devices, IoT, and bandwidth-constrained networks.
Why ECDH?
256-bit ECDH ≈ 3072-bit DH in security, but ECDH is 10-100× faster. Used in TLS 1.3, Bitcoin, Ethereum, Signal Protocol, WhatsApp, and modern VPNs.
Table of Contents
Theory
| Feature | ECDH | Traditional DH |
|---|---|---|
| Key Size (128-bit security) | 256 bits (32 bytes) | 3072 bits (384 bytes) |
| Public Key Size | 32-65 bytes | 384 bytes |
| Key Generation | ~5ms | ~80ms |
| Secret Computation | ~2ms | ~15ms |
| Performance | 10-100× faster | Slower |
| Bandwidth | Minimal | High |
| Battery Impact | Lower | Higher |
| Mobile Suitability | Excellent | Poor |
Conclusion: ECDH is the modern choice for key exchange on mobile and resource-constrained devices.
Class: ECDH
The ECDH class implements the Elliptic Curve Diffie-Hellman protocol. Instances are created using the createECDH() function.
ecdh.generateKeys()
Generates private and public ECDH key pair. Must be called before computeSecret().
Returns: Buffer - The public key in uncompressed format (0x04 + x + y coordinates)
Examples:
import { createECDH } from 'react-native-quick-crypto';
const ecdh = createECDH('prime256v1');
// Generate key pair
const publicKey = ecdh.generateKeys();
console.log('Public key length:', publicKey.length); // 65 bytes for prime256v1
// Public key format: 0x04 || x-coordinate || y-coordinate
console.log('Format byte:', publicKey[0].toString(16)); // '04'ecdh.computeSecret(otherPublicKey[, inputEncoding])
Computes the shared secret using the other party's public key.
Parameters:
| Name | Type | Description |
|---|---|---|
otherPublicKey | string | Buffer | The other party's public key |
inputEncoding | string | Encoding of otherPublicKey if it's a string: 'hex', 'base64', or 'base64url' |
Returns: Buffer - The computed shared secret (x-coordinate of the resulting elliptic curve point)
Important: Hash the shared secret before using it as an encryption key.
Examples:
import { createECDH } from 'react-native-quick-crypto';
// Alice's side
const alice = createECDH('secp256k1');
const alicePublicKey = alice.generateKeys();
// Bob's side
const bob = createECDH('secp256k1');
const bobPublicKey = bob.generateKeys();
// Compute shared secret
const aliceSecret = alice.computeSecret(bobPublicKey);
const bobSecret = bob.computeSecret(alicePublicKey);
console.log(aliceSecret.equals(bobSecret)); // true
// With hex encoding
const bobPublicKeyHex = bobPublicKey.toString('hex');
const aliceSecretFromHex = alice.computeSecret(bobPublicKeyHex, 'hex');ecdh.getPublicKey([encoding])
Returns the ECDH public key.
Parameters:
| Name | Type | Description |
|---|---|---|
encoding | string | Optional encoding: 'hex', 'base64', or 'base64url' |
Returns: Buffer or string
Examples:
const ecdh = createECDH('prime256v1');
ecdh.generateKeys();
const publicKey = ecdh.getPublicKey(); // Buffer
const publicKeyHex = ecdh.getPublicKey('hex'); // string
const publicKeyB64 = ecdh.getPublicKey('base64'); // stringecdh.getPrivateKey()
Returns the ECDH private key. Never transmit this value!
Returns: Buffer
Security Warning: Protect the private key - anyone who obtains it can compute all your shared secrets.
ecdh.setPublicKey(publicKey[, encoding])
Sets the ECDH public key. Useful for restoring state or testing.
Parameters:
| Name | Type | Description |
|---|---|---|
publicKey | string | Buffer | The public key to set |
encoding | string | Encoding of publicKey if it's a string |
ecdh.setPrivateKey(privateKey[, encoding])
Sets the ECDH private key. Useful for restoring state or key derivation scenarios.
Parameters:
| Name | Type | Description |
|---|---|---|
privateKey | string | Buffer | The private key to set |
encoding | string | Encoding of privateKey if it's a string |
Module Methods
createECDH(curveName)
Creates an ECDH instance using the specified elliptic curve.
Parameters:
| Name | Type | Description |
|---|---|---|
curveName | string | Name of the elliptic curve (see Supported Curves) |
Returns: ECDH instance
Examples:
import { createECDH } from 'react-native-quick-crypto';
// P-256 (NIST standard, widely supported)
const ecdh = createECDH('prime256v1');
// secp256k1 (Bitcoin/Ethereum)
const ecdh = createECDH('secp256k1');
// P-384 (higher security)
const ecdh = createECDH('secp384r1');Supported Curves
NIST Curves (Recommended for General Use)
| Curve Name | Also Known As | Key Size | Security Level | Use Case |
|---|---|---|---|---|
prime256v1 | P-256, secp256r1 | 256-bit | ~128-bit | General purpose, TLS, most APIs |
secp384r1 | P-384 | 384-bit | ~192-bit | High security applications |
secp521r1 | P-521 | 521-bit | ~256-bit | Maximum security |
Koblitz Curves
| Curve Name | Key Size | Security Level | Use Case |
|---|---|---|---|
secp256k1 | 256-bit | ~128-bit | Bitcoin, Ethereum, blockchain applications |
Recommendations:
- General purpose: Use
prime256v1(P-256) for maximum compatibility - Blockchain: Use
secp256k1for Bitcoin/Ethereum compatibility - High security: Use
secp384r1orsecp521r1
import { createECDH } from 'react-native-quick-crypto';
// ✅ Good - Widely supported, fast
const ecdh = createECDH('prime256v1');
// ✅ Good - If you need blockchain compatibility
const ecdh = createECDH('secp256k1');
// ✅ Better - Higher security
const ecdh = createECDH('secp384r1');Real-World Examples
Example 1: Secure WebSocket Connection
Establish encrypted WebSocket channel without pre-shared keys:
import {
createECDH,
createHash,
createCipheriv,
createDecipheriv,
randomBytes
} from 'react-native-quick-crypto';
class SecureWebSocket {
private ecdh: any;
private sessionKey?: Buffer;
constructor() {
this.ecdh = createECDH('prime256v1');
this.ecdh.generateKeys();
}
getPublicKey(): string {
return this.ecdh.getPublicKey('base64');
}
async establishEncryption(
ws: WebSocket,
peerPublicKeyB64: string
): Promise<void> {
const peerPublicKey = Buffer.from(peerPublicKeyB64, 'base64');
const sharedSecret = this.ecdh.computeSecret(peerPublicKey);
// Derive session key using HKDF-like approach
const hash = createHash('sha256');
hash.update(sharedSecret);
hash.update('websocket-encryption-v1');
this.sessionKey = hash.digest();
console.log('WebSocket encryption established');
}
encryptMessage(message: string): string {
if (!this.sessionKey) throw new Error('Encryption not established');
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', this.sessionKey, iv);
let encrypted = cipher.update(message, 'utf8', 'base64');
encrypted += cipher.final('base64');
const authTag = cipher.getAuthTag();
// Format: iv:authTag:ciphertext
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
}
decryptMessage(encryptedMessage: string): string {
if (!this.sessionKey) throw new Error('Encryption not established');
const [ivB64, tagB64, ciphertext] = encryptedMessage.split(':');
const iv = Buffer.from(ivB64, 'base64');
const authTag = Buffer.from(tagB64, 'base64');
const decipher = createDecipheriv('aes-256-gcm', this.sessionKey, iv);
decipher.setAuthTag(authTag);
let message = decipher.update(ciphertext, 'base64', 'utf8');
message += decipher.final('utf8');
return message;
}
}
// Client usage
const client = new SecureWebSocket();
const ws = new WebSocket('wss://example.com');
ws.onopen = () => {
// Send public key
ws.send(JSON.stringify({
type: 'key-exchange',
publicKey: client.getPublicKey()
}));
};
ws.onmessage = async (event) => {
const data = JSON.parse(event.data);
if (data.type === 'key-exchange') {
await client.establishEncryption(ws, data.publicKey);
// Now send encrypted messages
const encrypted = client.encryptMessage('Hello Server!');
ws.send(JSON.stringify({ type: 'encrypted', data: encrypted }));
} else if (data.type === 'encrypted') {
const decrypted = client.decryptMessage(data.data);
console.log('Received:', decrypted);
}
};Example 2: End-to-End Encrypted Chat
Full E2E encrypted messaging with key rotation:
import {
createECDH,
createHash,
randomBytes,
createCipheriv,
createDecipheriv
} from 'react-native-quick-crypto';
interface EncryptedMessage {
sessionId: string;
iv: string;
authTag: string;
ciphertext: string;
timestamp: number;
}
class E2EMessenger {
private sessions = new Map<string, Buffer>(); // sessionId -> key
private currentSessionId?: string;
// Create new session with Perfect Forward Secrecy
createSession(peerPublicKeyB64: string): {
sessionId: string;
publicKey: string;
} {
// Generate ephemeral ECDH keys for this session (PFS!)
const ecdh = createECDH('prime256v1');
const publicKey = ecdh.generateKeys();
// Compute shared secret
const peerPublicKey = Buffer.from(peerPublicKeyB64, 'base64');
const sharedSecret = ecdh.computeSecret(peerPublicKey);
// Derive session key
const hash = createHash('sha256');
hash.update(sharedSecret);
hash.update(randomBytes(16)); // Add randomness
const sessionKey = hash.digest();
// Store session
const sessionId = randomBytes(16).toString('hex');
this.sessions.set(sessionId, sessionKey);
this.currentSessionId = sessionId;
// Ephemeral keys are discarded after this function
// Even if compromised later, this session remains secure!
return {
sessionId,
publicKey: publicKey.toString('base64')
};
}
encryptMessage(
message: string,
sessionId?: string
): EncryptedMessage {
const sid = sessionId || this.currentSessionId;
if (!sid) throw new Error('No active session');
const sessionKey = this.sessions.get(sid);
if (!sessionKey) throw new Error('Session not found');
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', sessionKey, iv);
let ciphertext = cipher.update(message, 'utf8', 'base64');
ciphertext += cipher.final('base64');
const authTag = cipher.getAuthTag();
return {
sessionId: sid,
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
ciphertext,
timestamp: Date.now()
};
}
decryptMessage(encrypted: EncryptedMessage): string {
const sessionKey = this.sessions.get(encrypted.sessionId);
if (!sessionKey) throw new Error('Session not found or expired');
const iv = Buffer.from(encrypted.iv, 'base64');
const authTag = Buffer.from(encrypted.authTag, 'base64');
const decipher = createDecipheriv('aes-256-gcm', sessionKey, iv);
decipher.setAuthTag(authTag);
let message = decipher.update(encrypted.ciphertext, 'base64', 'utf8');
message += decipher.final('utf8');
return message;
}
rotateSession(peerPublicKeyB64: string) {
// Create new session (Perfect Forward Secrecy)
return this.createSession(peerPublicKeyB64);
}
expireSession(sessionId: string) {
this.sessions.delete(sessionId);
}
}
// Usage
const alice = new E2EMessenger();
const bob = new E2EMessenger();
// Initial key exchange
const aliceSession = alice.createSession(bob.createSession(
alice.createSession('dummy').publicKey
).publicKey);
const bobPublicKey = aliceSession.publicKey;
const bobSession = bob.createSession(bobPublicKey);
// Send encrypted message
const encrypted = alice.encryptMessage('Secret message', aliceSession.sessionId);
const decrypted = bob.decryptMessage(encrypted);
console.log(decrypted); // "Secret message"
// Rotate session after some time (PFS!)
setTimeout(() => {
const newSession = alice.rotateSession(bob.createSession('...').publicKey);
alice.expireSession(aliceSession.sessionId);
}, 3600000); // Rotate every hourExample 3: P2P File Encryption
Encrypt files for specific peers:
import {
createECDH,
createHash,
randomBytes,
createCipheriv,
createDecipheriv
} from 'react-native-quick-crypto';
import RNFS from 'react-native-fs';
class P2PFileEncryption {
private ecdh: any;
private publicKey: Buffer;
constructor() {
this.ecdh = createECDH('secp256k1');
this.publicKey = this.ecdh.generateKeys();
}
getPublicKey(): string {
return this.publicKey.toString('hex');
}
async encryptFile(
filePath: string,
recipientPublicKeyHex: string
): Promise<{ encryptedPath: string; metadata: any }> {
// Compute shared secret with recipient
const recipientPublicKey = Buffer.from(recipientPublicKeyHex, 'hex');
const sharedSecret = this.ecdh.computeSecret(recipientPublicKey);
// Derive file encryption key
const hash = createHash('sha256');
hash.update(sharedSecret);
hash.update('file-encryption');
const fileKey = hash.digest();
// Read file
const fileData = await RNFS.readFile(filePath, 'base64');
const fileBuffer = Buffer.from(fileData, 'base64');
// Encrypt file
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', fileKey, iv);
const encrypted = Buffer.concat([
cipher.update(fileBuffer),
cipher.final()
]);
// Save encrypted file
const encryptedPath = filePath + '.encrypted';
await RNFS.writeFile(
encryptedPath,
encrypted.toString('base64'),
'base64'
);
return {
encryptedPath,
metadata: {
iv: iv.toString('hex'),
originalName: filePath.split('/').pop(),
size: fileBuffer.length,
encryptedSize: encrypted.length
}
};
}
async decryptFile(
encryptedPath: string,
senderPublicKeyHex: string,
metadata: any,
outputPath: string
): Promise<void> {
// Compute shared secret with sender
const senderPublicKey = Buffer.from(senderPublicKeyHex, 'hex');
const sharedSecret = this.ecdh.computeSecret(senderPublicKey);
// Derive file encryption key (same as sender)
const hash = createHash('sha256');
hash.update(sharedSecret);
hash.update('file-encryption');
const fileKey = hash.digest();
// Read encrypted file
const encryptedData = await RNFS.readFile(encryptedPath, 'base64');
const encrypted = Buffer.from(encryptedData, 'base64');
// Decrypt file
const iv = Buffer.from(metadata.iv, 'hex');
const decipher = createDecipheriv('aes-256-cbc', fileKey, iv);
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
// Save decrypted file
await RNFS.writeFile(outputPath, decrypted.toString('base64'), 'base64');
}
}
// Usage
const alice = new P2PFileEncryption();
const bob = new P2PFileEncryption();
// Alice encrypts file for Bob
const { encryptedPath, metadata } = await alice.encryptFile(
'/path/to/document.pdf',
bob.getPublicKey()
);
// Send: encryptedPath, metadata, alice.getPublicKey() to Bob
// Bob decrypts
await bob.decryptFile(
encryptedPath,
alice.getPublicKey(),
metadata,
'/path/to/decrypted.pdf'
);Example 4: Derive Multiple Keys from Single Secret
Use ECDH secret to derive multiple purpose-specific keys:
import { createECDH, createHash } from 'react-native-quick-crypto';
function deriveMultipleKeys(
sharedSecret: Buffer,
...purposes: string[]
): Map<string, Buffer> {
const keys = new Map<string, Buffer>();
for (const purpose of purposes) {
const hash = createHash('sha256');
hash.update(sharedSecret);
hash.update(purpose);
keys.set(purpose, hash.digest());
}
return keys;
}
// Setup
const alice = createECDH('prime256v1');
const bob = createECDH('prime256v1');
const alicePublic = alice.generateKeys();
const bobPublic = bob.generateKeys();
const sharedSecret = alice.computeSecret(bobPublic);
// Derive multiple keys for different purposes
const keys = deriveMultipleKeys(
sharedSecret,
'encryption', // For message encryption
'authentication', // For message authentication
'file-encryption', // For file encryption
'metadata' // For metadata encryption
);
// Use purpose-specific keys
const encryptionKey = keys.get('encryption')!;
const authKey = keys.get('authentication')!;
const fileKey = keys.get('file-encryption')!;
console.log('Encryption key:', encryptionKey.toString('hex'));
console.log('Auth key:', authKey.toString('hex'));
console.log('File key:', fileKey.toString('hex'));Security Considerations
Critical Security Rules
- Use standard curves - P-256 (prime256v1) for general use
- Hash the shared secret - Never use raw ECDH output as encryption key
- Ephemeral keys - Generate new ECDH keys per session (Perfect Forward Secrecy)
- Validate public keys - Ensure received keys are valid curve points
- Authenticate peers - ECDH doesn't provide authentication
Best Practices
1. Curve Selection:
// ✅ Good - Widely supported, secure
const ecdh = createECDH('prime256v1');
// ✅ Good - If you need blockchain compatibility
const ecdh = createECDH('secp256k1');
// ❌ Avoid - Non-standard curves unless required2. Secret Derivation:
// ✅ Good - Hash the shared secret
const sharedSecret = ecdh.computeSecret(peerPublicKey);
const hash = createHash('sha256');
hash.update(sharedSecret);
const encryptionKey = hash.digest();
// ❌ Bad - Use raw secret
const sharedSecret = ecdh.computeSecret(peerPublicKey);
const cipher = createCipheriv('aes-256-cbc', sharedSecret, iv); // Weak!3. Perfect Forward Secrecy:
// ✅ Good - New ECDH instance per session
function newSession(peerPublicKey: Buffer) {
const ecdh = createECDH('prime256v1');
ecdh.generateKeys();
return ecdh.computeSecret(peerPublicKey);
}
// ❌ Bad - Reuse same ECDH instance
const ecdh = createECDH('prime256v1');
ecdh.generateKeys();
// ... reuse for multiple sessions4. Public Key Validation:
// ✅ Good: Validate received public keys
// try {
// const secret = ecdh.computeSecret(receivedPublicKey);
// } catch (error) {
// console.error('Invalid public key');
// }
// ❌ Bad: No validation
// const secret = ecdh.computeSecret(untrustedPublicKey); // Could throw!Common Errors
Error: Invalid key size
Cause: Public key has incorrect size for the curve.
Solution: Ensure both parties use the same curve:
// ❌ Wrong - Different curves
const alice = createECDH('prime256v1');
const bob = createECDH('secp384r1'); // Different!
// ✅ Correct - Same curve
const curve = 'prime256v1';
const alice = createECDH(curve);
const bob = createECDH(curve);Error: Point is not on curve
Cause: The provided public key is invalid or corrupted.
Solutions:
- Verify encoding consistency
- Check for transmission errors
- Ensure same curve on both sides
// ✅ Correct - Consistent encoding
const publicKey = alice.getPublicKey('hex');
const secret = bob.computeSecret(publicKey, 'hex');Different shared secrets
Cause: Using different curves or wrong public keys.
// ❌ Wrong: Using own public key
// const secret = alice.computeSecret(alice.getPublicKey()); // Wrong!
// ✅ Correct: Using peer's public key
const alice = createECDH('prime256v1');
const bob = createECDH('prime256v1');
alice.generateKeys();
bob.generateKeys();
const secret = alice.computeSecret(bob.getPublicKey());Performance Notes
ECDH Performance (prime256v1, typical mobile device):
- Key generation: ~5ms
- Secret computation: ~2ms
- Total: ~7ms per key exchange
Comparison with Traditional DH (modp15/3072-bit):
- Key generation: ~80ms
- Secret computation: ~15ms
- Total: ~95ms
ECDH is 13× faster!
Performance Tips
- Reuse ECDH instances within a session (but not across sessions)
- Pre-generate keys in background for immediate use
- Use prime256v1 for hardware acceleration on many devices
- Batch operations when establishing multiple connections
// Example: Pre-generate ECDH instances
const ecdhPool: any[] = [];
// Background task
for (let i = 0; i < 10; i++) {
const ecdh = createECDH('prime256v1');
ecdh.generateKeys();
ecdhPool.push(ecdh);
}
// Instant key exchange
function quickKeyExchange(peerPublicKey: Buffer): Buffer {
const ecdh = ecdhPool.pop();
if (!ecdh) throw new Error('Pool empty');
return ecdh.computeSecret(peerPublicKey);
}