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ā
| Issue | Cause | Solution |
|---|---|---|
| Shared secrets don't match | Wrong key usage | Verify key pair is correct |
| Decapsulation fails | Very rare failure | Implement fallback |
| Invalid key size | Wrong API usage | Check key sizes (1184, 64, 1088) |
| Slow operations | Not optimized | Check 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:
-
Simple API:
- Just use
encrypt(data, { publicKey }) - No need to handle KEM, AES-GCM, IV, salt manually
- Just use
-
Parameter management:
- Automatically generates random IV and salt
- Handles key derivation from shared secret
-
Consistent interface:
- Works like other cipher layers
- Easy to combine with other layers (AES-GCM, XChaCha20)
-
Less error-prone:
- Fewer steps to implement correctly
- Standardized parameter handling
-
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
Now let's explore advanced security and performance topics.