@omnituum/noise-kyber
v0.1.0
Published
Post-quantum secure Noise XX + ML-KEM device pairing for Loggie
Downloads
190
Maintainers
Readme
@loggie/noise-kyber
Post-quantum secure device pairing for Loggie
Implements Noise XX with hybrid X25519 + ML-KEM-768 (Kyber) for forward-secret, authenticated device synchronization.
Features
✅ Post-quantum security - Hybrid X25519 + ML-KEM-768 (Kyber) ✅ Forward secrecy - Fresh ephemeral keys every session ✅ Mutual authentication - Long-term static device keys ✅ PSK binding - QR token prevents MitM ✅ Compact QR codes - ~190 characters (hash-commit pattern) ✅ Auth code verification - User-friendly 15-char codes (ABC-DEF-GHI-JKL) ✅ Production-grade - Timing-safe, memory-zeroing, comprehensive tests
Installation
pnpm add @loggie/noise-kyberQuick Start
Device B (Shows QR Code)
import {
createPairingSession,
createQRFromSession,
PairingResponder
} from '@loggie/noise-kyber';
// 1. Create pairing session
const session = await createPairingSession(
'0x1234...', // LoggieCIDV3 identity anchor
'My Laptop' // Device label
);
// 2. Generate QR code
const qrCode = createQRFromSession(session);
displayQR(qrCode); // Show to user
// 3. Wait for WebRTC connection
const dataChannel = await waitForConnection();
// 4. Perform handshake
const responder = new PairingResponder(session);
dataChannel.onmessage = async (event) => {
const message = new Uint8Array(event.data);
// Receive message 1
await responder.receiveMessage1(message);
// Send message 2 + auth code
const { message: msg2, authCode, kyberPublicFull } = await responder.sendMessage2();
dataChannel.send(msg2);
dataChannel.send(new TextEncoder().encode(kyberPublicFull)); // Send full Kyber key
console.log('Show auth code to user:', authCode);
// User verifies codes match...
// Receive message 3
const msg3 = await receiveNextMessage();
const result = await responder.receiveMessage3(msg3);
console.log('Pairing complete!', result.transportKeys);
// Now use result.transportKeys for encrypted communication
};Device A (Scans QR Code)
import {
decodeQRPayload,
PairingInitiator
} from '@loggie/noise-kyber';
// 1. Scan and decode QR
const qrCode = await scanQRCode(); // User scans QR
const payload = decodeQRPayload(qrCode);
// 2. Connect via WebRTC
const dataChannel = await connectToDevice();
// 3. Perform handshake
const initiator = await PairingInitiator.create(
payload.token,
{ label: 'My Phone' }
);
// Send message 1
const msg1 = await initiator.sendMessage1();
dataChannel.send(msg1);
// Receive message 2 + Kyber key
const msg2 = await receiveNextMessage();
const kyberFull = await receiveNextMessage(); // Full Kyber key
const { authCode, remoteDeviceLabel, identityAnchor } =
await initiator.receiveMessage2(msg2, kyberFull);
console.log('Show auth code to user:', authCode);
console.log('Pairing with:', remoteDeviceLabel);
// User verifies codes match and approves...
// Send message 3
const result = await initiator.sendMessage3Approved();
console.log('Pairing complete!', result.transportKeys);API Reference
High-Level API
createPairingSession(identityAnchor, deviceLabel, staticKeys?)
Creates a new pairing session (Device B).
const session = await createPairingSession(
'0x1234...', // LoggieCIDV3 identity
'My Laptop', // Device name
staticKeys // Optional: long-term device keys
);createQRFromSession(session)
Generates compact QR payload (~190 chars).
const qrCode = createQRFromSession(session);
// qrCode: "3A7f2g9H..." (Base58)decodeQRPayload(qrCode)
Decodes and validates QR payload.
const payload = decodeQRPayload(qrCode);
// payload: { token, x25519Public, kyberPublicHash, ... }class PairingInitiator
Handles pairing from scanning device (Device A).
const initiator = await PairingInitiator.create(token, deviceInfo, staticKeys?);
await initiator.sendMessage1();
const { authCode } = await initiator.receiveMessage2(msg, kyberFull);
const result = await initiator.sendMessage3Approved();class PairingResponder
Handles pairing from QR-showing device (Device B).
const responder = new PairingResponder(session);
await responder.receiveMessage1(msg);
const { authCode } = await responder.sendMessage2();
const result = await responder.receiveMessage3(msg);Advanced API
Key Generation
import { generateEphemeralKeys, generateStaticKeys } from '@loggie/noise-kyber';
const ephemeral = await generateEphemeralKeys();
const static = await generateStaticKeys();Cryptographic Primitives
import {
deriveAuthCode,
deriveTransportKeys,
timingSafeEqual,
secureZero
} from '@loggie/noise-kyber';
const authCode = deriveAuthCode(transcriptHash);
const transportKeys = deriveTransportKeys(finalHash, 'initiator');
if (timingSafeEqual(code1, code2)) {
console.log('Auth codes match!');
}
secureZero(sensitiveData); // Zero memorySecurity
Threat Model
✅ Protects against:
- Man-in-the-middle attacks (PSK binding + auth codes)
- Post-quantum attacks (ML-KEM-768)
- Replay attacks (timestamp validation)
- Timing attacks (constant-time comparisons)
⚠️ Does NOT protect against:
- Compromised device endpoints
- Physical device theft (use biometric approval)
- Malicious QR codes (validate identity anchor)
Best Practices
- Always verify auth codes - Display on both devices
- Use static keys - Enables device authentication
- Validate timestamps - QR expires after 5 minutes
- Clean up - Call
.cleanup()after handshake - Use biometrics - Gate pairing approval with biometric check
Testing
# Run tests
pnpm test
# Watch mode
pnpm test:watch
# E2E tests
pnpm test:e2e
# Coverage
pnpm test --coverageArchitecture
Noise XX Pattern:
Device A (Initiator) Device B (Responder)
────────────────────────────────────────────────
1. e →
← 2. e, ee, s, es
3. s, se →
Where:
e = ephemeral key
s = static key
ee = ephemeral-ephemeral DH + KEM
es = ephemeral-static DH
se = static-ephemeral DHHybrid Post-Quantum KDF
PSK (QR token)
↓
X25519 DH + ML-KEM-768 encapsulation
↓
BLAKE3 transcript hashing
↓
HKDF-SHA256 key derivation
↓
ChaCha20-Poly1305 encryption
↓
Transport keysTroubleshooting
QR Code Too Large
Use hash-commit pattern (already default):
- QR contains Kyber public hash (32 bytes)
- Full key (1184 bytes) sent via WebRTC
- Keeps QR at ~190 chars
Timing Attacks
Use provided utilities:
import { timingSafeEqual } from '@loggie/noise-kyber';
// ❌ NEVER:
if (code1 === code2) { ... }
// ✅ ALWAYS:
if (timingSafeEqual(code1, code2)) { ... }Memory Safety
Always cleanup:
const responder = new PairingResponder(session);
try {
// ... handshake ...
} finally {
responder.cleanup(); // Zeros sensitive data
}License
MIT
Contributing
See CONTRIBUTING.md
Support
- GitHub Issues: github.com/loggie/issues
- Docs: docs.loggie.com
