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


Now let's explore advanced security and performance topics.

Next: Advanced Topics →