Duration: 75 minutes Prerequisites: Basic TypeScript knowledge, Elliptic Curve Fundamentals tutorial completed
Elliptic Curve Diffie-Hellman (ECDH) is a key agreement protocol that allows two parties to establish a shared secret over an unsecured communication channel. Unlike traditional encryption where you need to share a secret key beforehand, ECDH allows two parties who have never met to create a shared secret that only they know.
The mathematical foundation of ECDH relies on the commutative property of elliptic curve point multiplication:
(Alice's private key) × (Bob's public key)
(Bob's private key) × (Alice's public key)
import { PrivateKey, PublicKey, Point, BigNumber } from '@bsv/sdk'
Let’s start by generating key pairs for Alice and Bob:
function generateKeyPairs() {
console.log('=== Generating Key Pairs ===')
// Generate Alice's key pair
const alicePrivKey = PrivateKey.fromRandom()
const alicePubKey = alicePrivKey.toPublicKey()
// Generate Bob's key pair
const bobPrivKey = PrivateKey.fromRandom()
const bobPubKey = bobPrivKey.toPublicKey()
console.log('Alice private key:', alicePrivKey.toWif())
console.log('Alice public key:', alicePubKey.toString())
console.log('Bob private key:', bobPrivKey.toWif())
console.log('Bob public key:', bobPubKey.toString())
return { alicePrivKey, alicePubKey, bobPrivKey, bobPubKey }
}
Now both parties can derive the same shared secret:
function performECDH(alicePrivKey: PrivateKey, alicePubKey: PublicKey,
bobPrivKey: PrivateKey, bobPubKey: PublicKey) {
console.log('\n=== ECDH Key Exchange ===')
// Alice creates a shared secret using Bob's public key and her private key
const aliceSharedSecret = alicePrivKey.deriveSharedSecret(bobPubKey)
// Bob creates the same shared secret using Alice's public key and his private key
const bobSharedSecret = bobPrivKey.deriveSharedSecret(alicePubKey)
// Verify they're identical
const aliceSecretHex = aliceSharedSecret.getX().toHex()
const bobSecretHex = bobSharedSecret.getX().toHex()
console.log('Alice\'s shared secret (x-coordinate):', aliceSecretHex)
console.log('Bob\'s shared secret (x-coordinate):', bobSecretHex)
console.log('Secrets match:', aliceSecretHex === bobSecretHex)
return aliceSharedSecret
}
function basicECDHExample() {
try {
// Generate key pairs
const { alicePrivKey, alicePubKey, bobPrivKey, bobPubKey } = generateKeyPairs()
// Perform ECDH
const sharedSecret = performECDH(alicePrivKey, alicePubKey, bobPrivKey, bobPubKey)
// The shared secret is a point on the curve
console.log('\nShared secret point:')
console.log('X:', sharedSecret.getX().toHex())
console.log('Y:', sharedSecret.getY().toHex())
} catch (error) {
console.error('ECDH Error:', error.message)
}
}
// Run the example
basicECDHExample()
The SDK includes built-in security checks to prevent twist attacks:
function demonstrateSecurityValidation() {
console.log('\n=== Security Validation ===')
const validPrivKey = PrivateKey.fromRandom()
// This will work - valid public key
const validPubKey = PrivateKey.fromRandom().toPublicKey()
const validSecret = validPrivKey.deriveSharedSecret(validPubKey)
console.log('Valid ECDH succeeded')
// This will fail - invalid point (not on curve)
try {
const invalidPubKey = new PublicKey(new BigNumber(14), new BigNumber(16))
validPrivKey.deriveSharedSecret(invalidPubKey)
} catch (error) {
console.log('Security check prevented invalid key usage:', error.message)
}
}
demonstrateSecurityValidation()
Here’s how to use ECDH for encrypting messages:
import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'
function deriveEncryptionKey(sharedSecret: Point): Buffer {
// Use the x-coordinate of the shared secret as key material
const keyMaterial = sharedSecret.getX().toArray('be', 32)
// Hash to create a proper encryption key
return createHash('sha256').update(Buffer.from(keyMaterial)).digest()
}
function encryptMessage(message: string, sharedSecret: Point): {
encrypted: string,
iv: string
} {
const key = deriveEncryptionKey(sharedSecret)
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-cbc', key, iv)
let encrypted = cipher.update(message, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
encrypted,
iv: iv.toString('hex')
}
}
function decryptMessage(encryptedData: { encrypted: string, iv: string },
sharedSecret: Point): string {
const key = deriveEncryptionKey(sharedSecret)
const decipher = createDecipheriv('aes-256-cbc', key, Buffer.from(encryptedData.iv, 'hex'))
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
function secureMessagingExample() {
console.log('\n=== Secure Messaging with ECDH ===')
// Setup key pairs
const alicePrivKey = PrivateKey.fromRandom()
const bobPrivKey = PrivateKey.fromRandom()
const alicePubKey = alicePrivKey.toPublicKey()
const bobPubKey = bobPrivKey.toPublicKey()
// Derive shared secret
const sharedSecret = alicePrivKey.deriveSharedSecret(bobPubKey)
// Alice encrypts a message
const message = "Hello Bob! This is a secret message."
const encryptedData = encryptMessage(message, sharedSecret)
console.log('Original message:', message)
console.log('Encrypted:', encryptedData.encrypted)
// Bob decrypts the message using the same shared secret
const bobSharedSecret = bobPrivKey.deriveSharedSecret(alicePubKey)
const decryptedMessage = decryptMessage(encryptedData, bobSharedSecret)
console.log('Decrypted message:', decryptedMessage)
console.log('Messages match:', message === decryptedMessage)
}
secureMessagingExample()
Combine ECDH with digital signatures for authenticated key exchange:
function authenticatedKeyExchange() {
console.log('\n=== Authenticated Key Exchange ===')
// Generate long-term identity keys
const aliceIdentityPrivKey = PrivateKey.fromRandom()
const bobIdentityPrivKey = PrivateKey.fromRandom()
const aliceIdentityPubKey = aliceIdentityPrivKey.toPublicKey()
const bobIdentityPubKey = bobIdentityPrivKey.toPublicKey()
// Generate ephemeral keys for this session
const aliceEphemeralPrivKey = PrivateKey.fromRandom()
const bobEphemeralPrivKey = PrivateKey.fromRandom()
const aliceEphemeralPubKey = aliceEphemeralPrivKey.toPublicKey()
const bobEphemeralPubKey = bobEphemeralPrivKey.toPublicKey()
// Alice signs her ephemeral public key with her identity key
const aliceSignature = aliceIdentityPrivKey.sign(
Buffer.from(aliceEphemeralPubKey.toString(), 'utf8')
)
// Bob signs his ephemeral public key with his identity key
const bobSignature = bobIdentityPrivKey.sign(
Buffer.from(bobEphemeralPubKey.toString(), 'utf8')
)
// Verify signatures (in practice, you'd exchange these over the network)
const aliceSignatureValid = aliceIdentityPubKey.verify(
Buffer.from(aliceEphemeralPubKey.toString(), 'utf8'),
aliceSignature
)
const bobSignatureValid = bobIdentityPubKey.verify(
Buffer.from(bobEphemeralPubKey.toString(), 'utf8'),
bobSignature
)
console.log('Alice signature valid:', aliceSignatureValid)
console.log('Bob signature valid:', bobSignatureValid)
if (aliceSignatureValid && bobSignatureValid) {
// Perform ECDH with ephemeral keys
const sharedSecret = aliceEphemeralPrivKey.deriveSharedSecret(bobEphemeralPubKey)
console.log('Authenticated shared secret established')
console.log('Secret (x-coordinate):', sharedSecret.getX().toHex().substring(0, 16) + '...')
}
}
authenticatedKeyExchange()
Extend ECDH to multiple parties:
function multiPartyKeyAgreement() {
console.log('\n=== Multi-Party Key Agreement ===')
// Generate keys for three parties
const parties = ['Alice', 'Bob', 'Charlie'].map(name => ({
name,
privKey: PrivateKey.fromRandom(),
pubKey: null as PublicKey | null
}))
// Generate public keys
parties.forEach(party => {
party.pubKey = party.privKey.toPublicKey()
})
// Each party computes pairwise shared secrets
const sharedSecrets = new Map<string, Point>()
for (let i = 0; i < parties.length; i++) {
for (let j = i + 1; j < parties.length; j++) {
const party1 = parties[i]
const party2 = parties[j]
const secret = party1.privKey.deriveSharedSecret(party2.pubKey!)
const pairKey = `${party1.name}-${party2.name}`
sharedSecrets.set(pairKey, secret)
console.log(`${pairKey} shared secret:`, secret.getX().toHex().substring(0, 16) + '...')
}
}
return sharedSecrets
}
multiPartyKeyAgreement()
Use proper key derivation for different purposes:
function keyDerivationExample() {
console.log('\n=== Key Derivation Functions ===')
const alicePrivKey = PrivateKey.fromRandom()
const bobPrivKey = PrivateKey.fromRandom()
const sharedSecret = alicePrivKey.deriveSharedSecret(bobPrivKey.toPublicKey())
// Derive different keys for different purposes
function deriveKey(purpose: string, length: number = 32): Buffer {
const keyMaterial = sharedSecret.getX().toArray('be', 32)
const hash = createHash('sha256')
hash.update(Buffer.from(keyMaterial))
hash.update(Buffer.from(purpose, 'utf8'))
return hash.digest().slice(0, length)
}
const encryptionKey = deriveKey('encryption', 32)
const macKey = deriveKey('authentication', 32)
const ivKey = deriveKey('iv', 16)
console.log('Encryption key:', encryptionKey.toString('hex'))
console.log('MAC key:', macKey.toString('hex'))
console.log('IV key:', ivKey.toString('hex'))
}
keyDerivationExample()
function securityPitfalls() {
console.log('\n=== Security Pitfalls to Avoid ===')
// ❌ DON'T: Use shared secret directly as encryption key
console.log('❌ Never use the shared secret point directly for encryption')
// ✅ DO: Use proper key derivation
console.log('✅ Always use key derivation functions')
// ❌ DON'T: Reuse ephemeral keys
console.log('❌ Never reuse ephemeral keys across sessions')
// ✅ DO: Generate fresh ephemeral keys for each session
console.log('✅ Generate fresh keys for each exchange')
// ❌ DON'T: Skip public key validation
console.log('❌ Never skip public key validation')
// ✅ DO: Always validate received public keys
console.log('✅ SDK automatically validates keys in deriveSharedSecret()')
}
securityPitfalls()
function performanceExample() {
console.log('\n=== Performance Optimization ===')
const iterations = 1000
// Pre-generate keys
const privateKeys = Array.from({ length: iterations }, () => PrivateKey.fromRandom())
const publicKeys = privateKeys.map(pk => pk.toPublicKey())
// Measure ECDH performance
const startTime = Date.now()
for (let i = 0; i < iterations; i++) {
const sharedSecret = privateKeys[i].deriveSharedSecret(publicKeys[(i + 1) % iterations])
// In practice, you'd process the shared secret here
}
const endTime = Date.now()
const avgTime = (endTime - startTime) / iterations
console.log(`Performed ${iterations} ECDH operations`)
console.log(`Average time per operation: ${avgTime.toFixed(2)}ms`)
}
performanceExample()
function robustECDH(privateKey: PrivateKey, publicKey: PublicKey): Point | null {
try {
// Validate inputs
if (!privateKey || !publicKey) {
throw new Error('Invalid key parameters')
}
// Perform ECDH with built-in validation
const sharedSecret = privateKey.deriveSharedSecret(publicKey)
// Additional validation if needed
if (sharedSecret.getX().isZero() || sharedSecret.getY().isZero()) {
throw new Error('Invalid shared secret generated')
}
return sharedSecret
} catch (error) {
console.error('ECDH operation failed:', error.message)
return null
}
}
function errorHandlingExample() {
console.log('\n=== Error Handling ===')
const validPrivKey = PrivateKey.fromRandom()
const validPubKey = PrivateKey.fromRandom().toPublicKey()
// Test with valid keys
const result1 = robustECDH(validPrivKey, validPubKey)
console.log('Valid ECDH result:', result1 ? 'Success' : 'Failed')
// Test with invalid key (will be caught by SDK validation)
try {
const invalidPubKey = new PublicKey(new BigNumber(1), new BigNumber(1))
const result2 = robustECDH(validPrivKey, invalidPubKey)
console.log('Invalid ECDH result:', result2 ? 'Success' : 'Failed')
} catch (error) {
console.log('Caught invalid key error:', error.message)
}
}
errorHandlingExample()
function testECDHImplementation() {
console.log('\n=== ECDH Test Suite ===')
let passed = 0
let total = 0
function test(name: string, testFn: () => boolean) {
total++
try {
if (testFn()) {
console.log(`✅ ${name}`)
passed++
} else {
console.log(`❌ ${name}`)
}
} catch (error) {
console.log(`❌ ${name}: ${error.message}`)
}
}
// Test 1: Basic ECDH symmetry
test('Basic ECDH symmetry', () => {
const privA = PrivateKey.fromRandom()
const privB = PrivateKey.fromRandom()
const secretA = privA.deriveSharedSecret(privB.toPublicKey())
const secretB = privB.deriveSharedSecret(privA.toPublicKey())
return secretA.getX().toHex() === secretB.getX().toHex()
})
// Test 2: Different key formats
test('Different key formats', () => {
const privA = PrivateKey.fromRandom()
const privB = PrivateKey.fromRandom()
const pubB = PublicKey.fromString(privB.toPublicKey().toDER('hex') as string)
const secret1 = privA.deriveSharedSecret(privB.toPublicKey())
const secret2 = privA.deriveSharedSecret(pubB)
return secret1.getX().toHex() === secret2.getX().toHex()
})
// Test 3: Invalid key rejection
test('Invalid key rejection', () => {
const privKey = PrivateKey.fromRandom()
const invalidPubKey = new PublicKey(new BigNumber(14), new BigNumber(16))
try {
privKey.deriveSharedSecret(invalidPubKey)
return false // Should have thrown
} catch (error) {
return error.message.includes('not valid for ECDH')
}
})
console.log(`\nTest Results: ${passed}/${total} passed`)
return passed === total
}
testECDHImplementation()
In this tutorial, you’ve learned how to implement ECDH key exchange using the BSV TypeScript SDK. You now understand:
The BSV TypeScript SDK provides robust ECDH implementation with built-in security validations, making it safe and easy to implement secure key exchange protocols.