Skip to main content

Practical Implementation

Now that we understand the theory, let's learn how to use ML-KEM in real applications with code examples and best practices.

7.1 Installation and Setup

Install Dependencies

npm install @hpke/ml-kem

Basic Setup

import { MlKem768 } from '@hpke/ml-kem';

// Create ML-KEM instance
const kem = new MlKem768();

Other Variants

import { MlKem512, MlKem768, MlKem1024 } from '@hpke/ml-kem';

// Choose based on security requirements
const kem512 = new MlKem512(); // ~192-bit security
const kem768 = new MlKem768(); // ~256-bit security Recommended
const kem1024 = new MlKem1024(); // ~384-bit security

7.2 Complete Example

Full Working Example

import { MlKem768 } from '@hpke/ml-kem';
import crypto from 'crypto';

async function mlkemExample() {
console.log(' ML-KEM Complete Example\n');

// ============================================
// Step 1: Generate Key Pair (Receiver)
// ============================================
console.log(' Step 1: Generating key pair...');

const kem = new MlKem768();
const keyPair = await kem.generateKeyPair();

console.log(' Key pair generated');
console.log(` Public key size: ${keyPair.publicKey.key.length} bytes`);
console.log(` Private key size: ${keyPair.privateKey.key.length} bytes`);

// ============================================
// Step 2: Encapsulate (Sender)
// ============================================
console.log('\n Step 2: Encapsulating shared secret...');
console.log(' (Sender creates shared secret without knowing receiver\'s private key)');

const encapsulateResult = await kem.encap({
recipientPublicKey: keyPair.publicKey
});

console.log(' Encapsulation complete');
console.log(` Encapsulated key size: ${encapsulateResult.enc.length} bytes`);
console.log(` Shared secret size: ${encapsulateResult.sharedSecret.length} bytes`);

// ============================================
// Step 3: Decapsulate (Receiver)
// ============================================
console.log('\n Step 3: Decapsulating from encapsulated key...');
console.log(' (Receiver extracts shared secret using private key)');

const decapsulateResult = await kem.decap({
recipientKey: keyPair.privateKey,
enc: encapsulateResult.enc
});

console.log(' Decapsulation complete');

// ============================================
// Step 4: Verify Shared Secrets Match
// ============================================
console.log('\n🔍 Step 4: Verifying shared secrets...');

const senderSecret = encapsulateResult.sharedSecret;
const receiverSecret = decapsulateResult.sharedSecret;

const secretsMatch = arraysEqual(senderSecret, receiverSecret);

console.log(` Sender's secret: ${hexEncode(senderSecret)}`);
console.log(` Receiver's secret: ${hexEncode(receiverSecret)}`);
console.log(` Match: ${secretsMatch ? ' YES' : ' NO'}`);

if (secretsMatch) {
console.log('\n SUCCESS: Encapsulation/Decapsulation works perfectly!');
console.log(' Both parties have the same shared secret!');
}

return encapsulateResult.sharedSecret;
}

// Helper functions
function arraysEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}

function hexEncode(bytes: Uint8Array): string {
return Buffer.from(bytes).toString('hex').substring(0, 16) + '...';
}

// Run the example
mlkemExample().catch(console.error);

Output

 ML-KEM Complete Example

Step 1: Generating key pair...
Key pair generated
Public key size: 1184 bytes
Private key size: 64 bytes

Step 2: Encapsulating shared secret...
(Sender creates shared secret without knowing receiver's private key)
Encapsulation complete
Encapsulated key size: 1088 bytes
Shared secret size: 32 bytes

Step 3: Decapsulating from encapsulated key...
(Receiver extracts shared secret using private key)
Decapsulation complete

🔍 Step 4: Verifying shared secrets...
Sender's secret: 8f3a2b1c4d5e6f78...
Receiver's secret: 8f3a2b1c4d5e6f78...
Match: YES

SUCCESS: Encapsulation/Decapsulation works perfectly!
Both parties have the same shared secret!

7.3 Using with CascadingCipher

Note: CascadingCipher is a local project library. If using this code elsewhere, replace with your preferred abstraction layer.

High-Level Encryption API

import { MLKEMCipherLayer } from 'cryptography/CascadingCipher';

async function encryptWithCascadingCipher() {
// Create cipher layer
const layer = new MLKEMCipherLayer();

// Generate key pair
const keyPair = await layer.generateKeyPair();

// Encrypt
const message = 'Secret message with ML-KEM!';
const plaintext = new TextEncoder().encode(message);

const encrypted = await layer.encrypt(plaintext, {
publicKey: keyPair.publicKey
});

// Decrypt
const decrypted = await layer.decrypt(encrypted, {
privateKey: keyPair.privateKey
});

console.log(new TextDecoder().decode(decrypted));
}

Advantages of CascadingCipher

  • Simple API: Just encrypt/decrypt, handles KEM internally
  • Parameter management: Handles IV and salt generation
  • Key derivation: Derives AES keys from shared secret
  • Error handling: Consistent error messages

7.4 Secure Messaging System

Building a Secure Chat

import { MlKem768 } from '@hpke/ml-kem';
import crypto from 'crypto';

class SecureMessenger {
private kem: MlKem768;
private publicKey: Uint8Array | null = null;
private privateKey: Uint8Array | null = null;
private peerPublicKey: Uint8Array | null = null;

constructor() {
this.kem = new MlKem768();
}

// Generate key pair for this user
async generateKeys() {
const keyPair = await this.kem.generateKeyPair();
this.privateKey = keyPair.privateKey.key;
this.publicKey = keyPair.publicKey.key;
console.log('Keys generated');
}

// Get public key to share with others
getPublicKey(): Uint8Array {
if (!this.publicKey) throw new Error('Keys not generated');
return this.publicKey;
}

// Set peer's public key for encryption
setPeerPublicKey(pk: Uint8Array) {
this.peerPublicKey = pk;
}

// Encrypt message
async encrypt(message: string) {
if (!this.peerPublicKey) throw new Error('Peer public key not set');

// Encapsulate
const { sharedSecret, enc } = await this.kem.encap({
recipientPublicKey: { key: this.peerPublicKey }
});

// Encrypt message with AES-GCM
const plaintext = new TextEncoder().encode(message);
const key = await this.deriveKey(sharedSecret);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
plaintext
);

return {
encapsulated: enc,
ciphertext,
iv
};
}

// Decrypt message
async decrypt(encrypted: any) {
if (!this.privateKey) throw new Error('Private key not set');

// Decapsulate
const sharedSecret = await this.kem.decap({
recipientKey: { key: this.privateKey },
enc: encrypted.encapsulated
});

// Decrypt with AES-GCM
const key = await this.deriveKey(sharedSecret);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: encrypted.iv },
key,
encrypted.ciphertext
);

return new TextDecoder().decode(decrypted);
}

// Derive AES key from shared secret
private async deriveKey(secret: Uint8Array) {
return crypto.subtle.importKey(
'raw',
secret,
'AES-GCM',
false,
['encrypt', 'decrypt']
);
}
}

// Usage
async function messagingExample() {
// Create two users
const alice = new SecureMessenger();
const bob = new SecureMessenger();

// Generate keys
await alice.generateKeys();
await bob.generateKeys();

// Share public keys
alice.setPeerPublicKey(bob.getPublicKey());
bob.setPeerPublicKey(alice.getPublicKey());

// Alice sends message to Bob
const encrypted = await alice.encrypt('Hello Bob! This is secret.');
console.log('Alice encrypted:', Buffer.from(encrypted.ciphertext).toString('base64'));

// Bob decrypts
const decrypted = await bob.decrypt(encrypted);
console.log('Bob decrypted:', decrypted);
}

7.5 Best Practices

Key Storage

import crypto from 'crypto';

async function storePrivateKey(sk: Uint8Array, password: string) {
// Derive key from password
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey']
);

const encryptionKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: crypto.getRandomValues(new Uint8Array(16)),
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
'AES-GCM',
256,
['encrypt', 'decrypt']
);

// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
encryptionKey,
sk
);

// Store encrypted + iv
return { encrypted, iv };
}

Error Handling

async function safeEncapsulation(publicKey: Uint8Array) {
try {
const kem = new MlKem768();
const result = await kem.encap({
recipientPublicKey: { key: publicKey }
});

// Validate
if (!result.enc || result.enc.length !== 1088) {
throw new Error('Invalid encapsulated key');
}
if (!result.sharedSecret || result.sharedSecret.length !== 32) {
throw new Error('Invalid shared secret');
}

return result;
} catch (error) {
console.error('Encapsulation failed:', error);
throw error;
}
}

Key Rotation

class SessionKeyManager {
private static KEY_TTL = 24 * 60 * 60 * 1000; // 24 hours
private keyStartTime: number = 0;

async generateKeys() {
const kem = new MlKem768();
const keyPair = await kem.generateKeyPair();
this.keyStartTime = Date.now();
return keyPair;
}

async shouldRotate() {
const age = Date.now() - this.keyStartTime;
return age > SessionKeyManager.KEY_TTL;
}

async rotateKeys() {
console.log('Rotating keys...');
return this.generateKeys();
}
}

7.6 Testing and Verification

Unit Testing

import { MlKem768 } from '@hpke/ml-kem';

describe('ML-KEM Tests', () => {
test('encapsulation and decapsulation produce same secret', async () => {
const kem = new MlKem768();
const keyPair = await kem.generateKeyPair();

const encapsulateResult = await kem.encap({
recipientPublicKey: keyPair.publicKey
});

const decapsulateResult = await kem.decap({
recipientKey: keyPair.privateKey,
enc: encapsulateResult.enc
});

expect(encapsulateResult.sharedSecret).toEqual(decapsulateResult.sharedSecret);
});

test('key generation produces valid sizes', async () => {
const kem = new MlKem768();
const keyPair = await kem.generateKeyPair();

expect(keyPair.publicKey.key.length).toBe(1184);
expect(keyPair.privateKey.key.length).toBe(64);
});
});

Interactive Demo

Note: The interactive demo requires the cryptography website to be hosted. If not available, you can run the code examples locally in your environment instead.

7.7 Debugging Tips

Common Issues

IssueCauseSolution
Shared secrets don't matchWrong key usageVerify key pair is correct
Decapsulation failsVery rare failureImplement fallback
Invalid key sizeWrong API usageCheck key sizes (1184, 64, 1088)
Slow operationsNot optimizedCheck browser compatibility

Logging

//  Good: Log operations, not secrets
console.log('Generated key pair');
console.log('Encapsulation complete');
console.log('Decapsulation complete');

// Bad: Never log secrets!
// console.log('Shared secret:', sharedSecret);
// console.log('Private key:', privateKey);

Performance Benchmarks

async function benchmark() {
const kem = new MlKem768();

const startKeyGen = performance.now();
await kem.generateKeyPair();
const endKeyGen = performance.now();

const keyPair = await kem.generateKeyPair();
const startEncap = performance.now();
await kem.encap({ recipientPublicKey: keyPair.publicKey });
const endEncap = performance.now();

console.log('Key generation:', (endKeyGen - startKeyGen).toFixed(2), 'ms');
console.log('Encapsulation:', (endEncap - startEncap).toFixed(2), 'ms');
}

7.8 Example Applications

File Encryption

async function encryptFile(file: File) {
const kem = new MlKem768();
const layer = new MLKEMCipherLayer();
const keyPair = await kem.generateKeyPair();

const ArrayBuffer = await file.arrayBuffer();
const encrypted = await layer.encrypt(new Uint8Array(ArrayBuffer), {
publicKey: keyPair.publicKey
});

return { encrypted, publicKey: keyPair.publicKey };
}

async function decryptFile(encryptedData: any, privateKey: Uint8Array) {
const layer = new MLKEMCipherLayer();
return layer.decrypt(encryptedData, { privateKey });
}

API Security

// Express middleware for API security
import express from 'express';

async function mlkemAuth(req: express.Request, res: express.Response, next: any) {
// Validate encapsulated key
const encapsulatedKey = req.headers['x-encapsulated-key'];
if (!encapsulatedKey) {
return res.status(401).json({ error: 'No key provided' });
}

// Decrypt and verify
const keyPair = await loadKeyPair();
try {
const kem = new MlKem768();
await kem.decap({
recipientKey: keyPair.privateKey,
enc: Buffer.from(encapsulatedKey as string, 'base64')
});
next();
} catch (error) {
res.status(401).json({ error: 'Invalid key' });
}
}

Quiz

Question: What are the main advantages of using CascadingCipher's MLKEMCipherLayer instead of directly using MlKem768?

Show Answer

CascadingCipher advantages:

  1. Simple API:

    • Just use encrypt(data, { publicKey })
    • No need to handle KEM, AES-GCM, IV, salt manually
  2. Parameter management:

    • Automatically generates random IV and salt
    • Handles key derivation from shared secret
  3. Consistent interface:

    • Works like other cipher layers
    • Easy to combine with other layers (AES-GCM, XChaCha20)
  4. Less error-prone:

    • Fewer steps to implement correctly
    • Standardized parameter handling
  5. Extensible:

    • Can combine layers for defense-in-depth
    • Easy to swap implementations

When to use MlKem768 directly:

  • Maximum control over implementation
  • Custom key derivation
  • Specific protocol requirements
  • Integration with existing systems

Key Takeaways

Install: npm install @hpke/ml-kem Use: MlKem768 is recommended variant Pattern: Encapsulate → Derive AES key → Encrypt data Storage: Encrypt private keys, store public keys openly Testing: Always verify shared secrets match Best practices: Validate inputs, handle errors, rotate keys