ts-sdk

ECDH Key Exchange

Duration: 75 minutes Prerequisites: Basic TypeScript knowledge, Elliptic Curve Fundamentals tutorial completed

Learning Goals

Introduction to ECDH

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:

Setting Up Your Environment

import { PrivateKey, PublicKey, Point, BigNumber } from '@bsv/sdk'

Basic ECDH Key Exchange

Step 1: Key Generation

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 }
}

Step 2: Deriving Shared Secrets

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
}

Step 3: Complete Example

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()

Security Validation

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()

Practical Applications

Secure Message Exchange

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()

Key Exchange with Authentication

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()

Advanced ECDH Patterns

Multi-Party Key Agreement

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()

Key Derivation Functions

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()

Security Considerations

Best Practices

  1. Key Validation: Always validate public keys before use
  2. Ephemeral Keys: Use ephemeral keys for forward secrecy
  3. Authentication: Combine with signatures to prevent man-in-the-middle attacks
  4. Key Derivation: Use proper KDFs to derive encryption keys from shared secrets

Common Pitfalls

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()

Performance Considerations

Optimizing ECDH Operations

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()

Error Handling

Robust ECDH Implementation

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()

Testing Your ECDH Implementation

Comprehensive Test Suite

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()

Conclusion

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.

Next Steps

Further Reading