@omnituum/noise-kyber
v0.2.1
Published
Post-quantum secure Noise XX + ML-KEM device pairing for Loggie
Downloads
218
Maintainers
Readme
@omnituum/noise-kyber
Post-quantum secure device pairing for Loggie
Implements Noise XX with hybrid X25519 + ML-KEM-1024 (Kyber) for forward-secret, authenticated device synchronization.
Features
✅ Post-quantum security - Hybrid X25519 + ML-KEM-1024 (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) ✅ Defensive implementation - Timing-safe comparisons, best-effort memory zeroing, end-to-end handshake tests
Status: internal Loggie/Omnituum component. The protocol is a custom Noise XX variant that has not been externally audited or formally verified. Do not treat it as a drop-in replacement for an audited Noise implementation.
Installation
pnpm add @omnituum/noise-kyberQuick Start
Device B (Shows QR Code)
import {
createPairingSession,
createQRFromSession,
PairingResponder
} from '@omnituum/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 '@omnituum/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?, // defaults to fresh static keys
timeoutMs?, // defaults to HANDSHAKE_TIMEOUT_MS (60s)
expectedKyberPublicHash? // pass payload.kyberPublicHash to enforce the QR commitment
);
await initiator.sendMessage1();
const { authCode } = await initiator.receiveMessage2(msg, kyberFull);
const result = await initiator.sendMessage3Approved();When expectedKyberPublicHash is provided (the 32-byte kyberPublicHash from
the decoded QR payload), receiveMessage2 throws KyberCommitmentMismatchError
and discards the session unless the responder's ephemeral Kyber key hashes to
that commitment. Without it, the commitment is not checked — MitM resistance
then rests on the PSK/token binding alone.
class PairingResponder
Handles pairing from QR-showing device (Device B).
const responder = new PairingResponder(session, timeoutMs?);
await responder.receiveMessage1(msg);
const { authCode } = await responder.sendMessage2();
const result = await responder.receiveMessage3(msg);Handshake timeouts
Both classes enforce a time budget (timeoutMs, default HANDSHAKE_TIMEOUT_MS
= 60 s) through two complementary mechanisms:
- Late-step rejection — any handshake method called after the budget has
elapsed zeroes the session secrets and throws
HandshakeTimeoutError. - Automatic abandoned-session cleanup — a timer zeroes the session secrets
when the budget elapses without completion, even if no further handshake
method is ever called (e.g. a peer sends message 1 and vanishes). The
instance emits
'handshake:expired'when this fires. The timer is cleared on successful completion or explicitcleanup(), and isunref()ed so it never keeps a Node.js process alive.
cleanup() is idempotent and safe to call at any point.
Advanced API
Key Generation
import { generateEphemeralKeys, generateStaticKeys } from '@omnituum/noise-kyber';
const ephemeral = await generateEphemeralKeys();
const static = await generateStaticKeys();Cryptographic Primitives
import {
deriveAuthCode,
deriveTransportKeys,
timingSafeEqual,
secureZero
} from '@omnituum/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-1024)
- 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-1024 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 sent in message 2
- Keeps QR at ~190 chars
- Pass
payload.kyberPublicHashasexpectedKyberPublicHashtoPairingInitiator.createto enforce the commitment; it is not checked otherwise
Timing Attacks
Use provided utilities:
import { timingSafeEqual } from '@omnituum/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
