@omnituum/pqc-shared 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/crypto/index.cjs +81 -54
  2. package/dist/crypto/index.d.cts +33 -11
  3. package/dist/crypto/index.d.ts +33 -11
  4. package/dist/crypto/index.js +81 -54
  5. package/dist/{decrypt-eSHlbh1j.d.cts → decrypt-O1iqFIDL.d.cts} +80 -6
  6. package/dist/{decrypt-eSHlbh1j.d.ts → decrypt-O1iqFIDL.d.ts} +80 -6
  7. package/dist/fs/index.cjs +286 -147
  8. package/dist/fs/index.d.cts +28 -7
  9. package/dist/fs/index.d.ts +28 -7
  10. package/dist/fs/index.js +277 -147
  11. package/dist/index.cjs +358 -229
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +357 -230
  15. package/dist/{integrity-BenoFsmP.d.cts → integrity-BkBDrHA-.d.cts} +12 -4
  16. package/dist/{integrity-D9J98Bty.d.ts → integrity-DsFJv5c-.d.ts} +12 -4
  17. package/dist/{types-CRka8PC7.d.cts → types-DmnVueAY.d.cts} +14 -4
  18. package/dist/{types-CRka8PC7.d.ts → types-DmnVueAY.d.ts} +14 -4
  19. package/dist/utils/index.cjs +7 -10
  20. package/dist/utils/index.d.cts +2 -2
  21. package/dist/utils/index.d.ts +2 -2
  22. package/dist/utils/index.js +7 -10
  23. package/dist/vault/index.cjs +8 -11
  24. package/dist/vault/index.d.cts +2 -2
  25. package/dist/vault/index.d.ts +2 -2
  26. package/dist/vault/index.js +8 -11
  27. package/package.json +7 -7
  28. package/src/crypto/dilithium.ts +4 -4
  29. package/src/crypto/hybrid.ts +182 -80
  30. package/src/crypto/index.ts +2 -0
  31. package/src/fs/decrypt.ts +145 -68
  32. package/src/fs/encrypt.ts +161 -112
  33. package/src/fs/format.ts +110 -15
  34. package/src/fs/index.ts +4 -0
  35. package/src/fs/types.ts +77 -6
  36. package/src/index.ts +4 -0
  37. package/src/utils/integrity.ts +16 -15
  38. package/src/vault/manager.ts +1 -1
  39. package/src/version.ts +29 -7
@@ -261,8 +261,7 @@ async function loadDilithium() {
261
261
  const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
262
262
  dilithiumModule = ml_dsa65;
263
263
  return dilithiumModule;
264
- } catch (e) {
265
- console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
264
+ } catch {
266
265
  return null;
267
266
  }
268
267
  }
@@ -280,8 +279,7 @@ async function generateDilithiumKeypair() {
280
279
  publicB64: toB64(kp.publicKey),
281
280
  secretB64: toB64(kp.secretKey)
282
281
  };
283
- } catch (e) {
284
- console.warn("[Dilithium] Key generation failed:", e);
282
+ } catch {
285
283
  return null;
286
284
  }
287
285
  }
@@ -350,11 +348,13 @@ var DILITHIUM_SECRET_KEY_SIZE = 4032;
350
348
  var DILITHIUM_SIGNATURE_SIZE = 3309;
351
349
  var DILITHIUM_ALGORITHM = "ML-DSA-65";
352
350
  var ENVELOPE_VERSION = envelopeRegistry.OMNI_VERSIONS.HYBRID_V1;
351
+ var ENVELOPE_VERSION_V2 = envelopeRegistry.OMNI_VERSIONS.HYBRID_V2;
353
352
  var ENVELOPE_VERSION_LEGACY = envelopeRegistry.DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
354
- var ENVELOPE_SUITE = "x25519+kyber768";
353
+ var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
355
354
  var ENVELOPE_AEAD = "xsalsa20poly1305";
356
355
  var SUPPORTED_ENVELOPE_VERSIONS = [
357
356
  ENVELOPE_VERSION,
357
+ ENVELOPE_VERSION_V2,
358
358
  ENVELOPE_VERSION_LEGACY
359
359
  ];
360
360
  var VersionMismatchError = class extends Error {
@@ -386,7 +386,6 @@ async function generateHybridIdentity(name) {
386
386
  const x25519 = generateX25519Keypair();
387
387
  const kyber = await generateKyberKeypair();
388
388
  if (!kyber) {
389
- console.error("Kyber key generation failed - library not available");
390
389
  return null;
391
390
  }
392
391
  return {
@@ -428,49 +427,74 @@ function getSecretKeys(identity) {
428
427
  kyberSecB64: identity.kyberSecB64
429
428
  };
430
429
  }
430
+ function combinedKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
431
+ const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
432
+ ikm.set(kyberShared, 0);
433
+ ikm.set(x25519Shared, kyberShared.length);
434
+ try {
435
+ return hkdfFlex(ikm, "omnituum/hybrid-v2", `wrap-ck|${x25519EpkHex}|${kyberKemCtB64}`);
436
+ } finally {
437
+ ikm.fill(0);
438
+ }
439
+ }
431
440
  async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
432
441
  const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
433
442
  const CK = rand32();
434
- const contentNonce = rand24();
435
- const ciphertext = nacl__default.default.secretbox(pt, contentNonce, CK);
436
- const x25519EphKp = nacl__default.default.box.keyPair();
437
- const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
438
- const x25519Shared = nacl__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
439
- const x25519Kek = hkdfFlex(x25519Shared, "omnituum/x25519", "wrap-ck");
440
- const x25519WrapNonce = rand24();
441
- const x25519Wrapped = nacl__default.default.secretbox(CK, x25519WrapNonce, x25519Kek);
442
- const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
443
- const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/kyber", "wrap-ck");
444
- const kyberWrapNonce = rand24();
445
- const kyberWrapped = nacl__default.default.secretbox(CK, kyberWrapNonce, kyberKek);
446
- return {
447
- v: ENVELOPE_VERSION,
448
- suite: ENVELOPE_SUITE,
449
- aead: ENVELOPE_AEAD,
450
- x25519Epk: toHex(x25519EphKp.publicKey),
451
- x25519Wrap: {
452
- nonce: b64(x25519WrapNonce),
453
- wrapped: b64(x25519Wrapped)
454
- },
455
- kyberKemCt: b64(kyberResult.ciphertext),
456
- kyberWrap: {
457
- nonce: b64(kyberWrapNonce),
458
- wrapped: b64(kyberWrapped)
459
- },
460
- contentNonce: b64(contentNonce),
461
- ciphertext: b64(ciphertext),
462
- meta: {
463
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
464
- senderName: sender?.name,
465
- senderId: sender?.id
466
- }
467
- };
443
+ try {
444
+ const contentNonce = rand24();
445
+ const ciphertext = nacl__default.default.secretbox(pt, contentNonce, CK);
446
+ const x25519EphKp = nacl__default.default.box.keyPair();
447
+ const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
448
+ const x25519Shared = nacl__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
449
+ const x25519EpkHex = toHex(x25519EphKp.publicKey);
450
+ const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
451
+ const kyberKemCtB64 = b64(kyberResult.ciphertext);
452
+ const kek = combinedKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
453
+ const ckWrapNonce = rand24();
454
+ const ckWrapped = nacl__default.default.secretbox(CK, ckWrapNonce, kek);
455
+ kek.fill(0);
456
+ x25519Shared.fill(0);
457
+ kyberResult.sharedSecret.fill(0);
458
+ x25519EphKp.secretKey.fill(0);
459
+ return {
460
+ v: ENVELOPE_VERSION_V2,
461
+ suite: ENVELOPE_SUITE_V2,
462
+ aead: ENVELOPE_AEAD,
463
+ x25519Epk: x25519EpkHex,
464
+ kyberKemCt: kyberKemCtB64,
465
+ ckWrap: {
466
+ nonce: b64(ckWrapNonce),
467
+ wrapped: b64(ckWrapped)
468
+ },
469
+ contentNonce: b64(contentNonce),
470
+ ciphertext: b64(ciphertext),
471
+ meta: {
472
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
473
+ senderName: sender?.name,
474
+ senderId: sender?.id
475
+ }
476
+ };
477
+ } finally {
478
+ CK.fill(0);
479
+ }
480
+ }
481
+ async function unwrapCkV2(envelope, secretKeys) {
482
+ const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
483
+ const ephPk = fromHex(envelope.x25519Epk);
484
+ const sk = fromHex(secretKeys.x25519SecHex);
485
+ const x25519Shared = nacl__default.default.scalarMult(sk, ephPk);
486
+ const kek = combinedKekV2(kyberShared, x25519Shared, envelope.x25519Epk, envelope.kyberKemCt);
487
+ kyberShared.fill(0);
488
+ x25519Shared.fill(0);
489
+ const CK = nacl__default.default.secretbox.open(ub64(envelope.ckWrap.wrapped), ub64(envelope.ckWrap.nonce), kek);
490
+ kek.fill(0);
491
+ if (!CK) {
492
+ throw new Error("Could not unwrap content key \u2014 combined-KEK authentication failed");
493
+ }
494
+ return CK;
468
495
  }
469
- async function hybridDecrypt(envelope, secretKeys) {
470
- const version = envelope.v;
471
- assertEnvelopeVersion(version);
496
+ async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
472
497
  let CK = null;
473
- const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
474
498
  try {
475
499
  const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
476
500
  const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
@@ -479,11 +503,7 @@ async function hybridDecrypt(envelope, secretKeys) {
479
503
  ub64(envelope.kyberWrap.nonce),
480
504
  kyberKek
481
505
  );
482
- if (CK) {
483
- console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
484
- }
485
- } catch (e) {
486
- console.warn("[Hybrid] Kyber decapsulation failed:", e);
506
+ } catch {
487
507
  }
488
508
  if (!CK) {
489
509
  try {
@@ -496,21 +516,28 @@ async function hybridDecrypt(envelope, secretKeys) {
496
516
  ub64(envelope.x25519Wrap.nonce),
497
517
  x25519Kek
498
518
  );
499
- if (CK) {
500
- console.log("[Hybrid] Decrypted using X25519 (classical)");
501
- }
502
- } catch (e) {
503
- console.warn("[Hybrid] X25519 decryption failed:", e);
519
+ } catch {
504
520
  }
505
521
  }
506
522
  if (!CK) {
507
523
  throw new Error("Could not unwrap content key with either algorithm");
508
524
  }
525
+ return CK;
526
+ }
527
+ async function hybridDecrypt(envelope, secretKeys) {
528
+ const version = envelope.v;
529
+ assertEnvelopeVersion(version);
530
+ const CK = version === ENVELOPE_VERSION_V2 ? await unwrapCkV2(envelope, secretKeys) : await unwrapCkV1(
531
+ envelope,
532
+ secretKeys,
533
+ version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum"
534
+ );
509
535
  const pt = nacl__default.default.secretbox.open(
510
536
  ub64(envelope.ciphertext),
511
537
  ub64(envelope.contentNonce),
512
538
  CK
513
539
  );
540
+ CK.fill(0);
514
541
  if (!pt) {
515
542
  throw new Error("Content authentication failed");
516
543
  }
@@ -1,4 +1,4 @@
1
- import { OmniHybridV1 } from '@omnituum/envelope-registry';
1
+ import { OmniHybridV2, OmniHybridV1 } from '@omnituum/envelope-registry';
2
2
  import * as _noble_ciphers_utils_js from '@noble/ciphers/utils.js';
3
3
 
4
4
  /**
@@ -77,10 +77,16 @@ declare function kyberUnwrapKey(sharedSecret: Uint8Array, nonceB64: string, wrap
77
77
  *
78
78
  * Post-quantum secure hybrid encryption combining:
79
79
  * - X25519 ECDH (classical, proven security)
80
- * - Kyber ML-KEM-768 (post-quantum, NIST Level 3)
80
+ * - ML-KEM-1024 (post-quantum, FIPS 203, NIST Level 5)
81
81
  *
82
- * Both key exchanges must succeed for decryption, providing
83
- * security against both classical and quantum attacks.
82
+ * v2 (current write format): the content key is wrapped once, under a KEK
83
+ * derived from BOTH shared secrets (HKDF(ss_mlkem || ss_x25519) with
84
+ * transcript binding) — both key exchanges must succeed to decrypt, so
85
+ * breaking either primitive alone is insufficient.
86
+ *
87
+ * v1 (read-only legacy): wrapped the key independently under each
88
+ * primitive; either secret alone sufficed. Kept only for decrypting
89
+ * existing envelopes — see the 2026-07-05 security audit.
84
90
  */
85
91
 
86
92
  interface HybridIdentity {
@@ -116,12 +122,21 @@ interface HybridSecretKeys {
116
122
  kyberSecB64: string;
117
123
  }
118
124
 
119
- type HybridEnvelope = Omit<OmniHybridV1, 'meta'> & {
125
+ type HybridEnvelopeV1 = Omit<OmniHybridV1, 'meta'> & {
120
126
  meta: OmniHybridV1['meta'] & {
121
127
  senderName?: string;
122
128
  senderId?: string;
123
129
  };
124
130
  };
131
+
132
+ type HybridEnvelopeV2 = Omit<OmniHybridV2, 'meta'> & {
133
+ meta: OmniHybridV2['meta'] & {
134
+ senderName?: string;
135
+ senderId?: string;
136
+ };
137
+ };
138
+ /** Any hybrid envelope this module can decrypt. */
139
+ type HybridEnvelope = HybridEnvelopeV1 | HybridEnvelopeV2;
125
140
  /**
126
141
  * Generate a new hybrid identity with both X25519 and Kyber keys.
127
142
  */
@@ -139,7 +154,12 @@ declare function getPublicKeys(identity: HybridIdentity): HybridPublicKeys;
139
154
  */
140
155
  declare function getSecretKeys(identity: HybridIdentity): HybridSecretKeys;
141
156
  /**
142
- * Encrypt a message using hybrid X25519 + Kyber encryption.
157
+ * Encrypt a message using hybrid X25519 + ML-KEM-1024 encryption (v2).
158
+ *
159
+ * The content key is wrapped exactly once, under a KEK derived from both
160
+ * shared secrets together — breaking either primitive alone is insufficient
161
+ * to unwrap. (v1 wrapped the key independently under each primitive, which
162
+ * reduced security to min(X25519, ML-KEM); v1 is now read-only legacy.)
143
163
  *
144
164
  * @param plaintext - Message to encrypt (string or bytes)
145
165
  * @param recipientPublicKeys - Recipient's public keys
@@ -148,12 +168,14 @@ declare function getSecretKeys(identity: HybridIdentity): HybridSecretKeys;
148
168
  declare function hybridEncrypt(plaintext: string | Uint8Array, recipientPublicKeys: HybridPublicKeys, sender?: {
149
169
  name?: string;
150
170
  id?: string;
151
- }): Promise<HybridEnvelope>;
171
+ }): Promise<HybridEnvelopeV2>;
152
172
  /**
153
- * Decrypt a hybrid envelope.
173
+ * Decrypt a hybrid envelope (v2, v1, or legacy pqc-demo).
154
174
  *
155
- * Tries Kyber first (post-quantum), falls back to X25519 (classical).
156
- * Both must be valid for the envelope to be considered secure.
175
+ * v2: the content key is unwrapped from a single AND-combined KEK — both
176
+ * ML-KEM and X25519 secrets are required, and any failure is fatal.
177
+ * v1/legacy: read-only compatibility path (either secret suffices — the
178
+ * defect v2 exists to fix).
157
179
  *
158
180
  * @param envelope - Encrypted envelope
159
181
  * @param secretKeys - Recipient's secret keys
@@ -641,4 +663,4 @@ declare const BOX_KEY_SIZE: number;
641
663
  /** NaCl box nonce size (24 bytes) */
642
664
  declare const BOX_NONCE_SIZE: number;
643
665
 
644
- export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, type Blake3Options, type BoxPayload, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, type ChaChaPayload, type ClassicalWrap, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, type DilithiumKeypair, type DilithiumKeypairB64, type DilithiumSignature, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, type HkdfHash, type HkdfOptions, type HybridEnvelope, type HybridIdentity, type HybridPublicKeys, type HybridSecretKeys, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, type KyberEncapsulation, type KyberKeypair, type KyberKeypairB64, type KyberSuite, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, type SecretboxPayload, type X25519Keypair, type X25519KeypairHex, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
666
+ export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, type Blake3Options, type BoxPayload, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, type ChaChaPayload, type ClassicalWrap, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, type DilithiumKeypair, type DilithiumKeypairB64, type DilithiumSignature, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, type HkdfHash, type HkdfOptions, type HybridEnvelope, type HybridEnvelopeV1, type HybridEnvelopeV2, type HybridIdentity, type HybridPublicKeys, type HybridSecretKeys, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, type KyberEncapsulation, type KyberKeypair, type KyberKeypairB64, type KyberSuite, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, type SecretboxPayload, type X25519Keypair, type X25519KeypairHex, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
@@ -1,4 +1,4 @@
1
- import { OmniHybridV1 } from '@omnituum/envelope-registry';
1
+ import { OmniHybridV2, OmniHybridV1 } from '@omnituum/envelope-registry';
2
2
  import * as _noble_ciphers_utils_js from '@noble/ciphers/utils.js';
3
3
 
4
4
  /**
@@ -77,10 +77,16 @@ declare function kyberUnwrapKey(sharedSecret: Uint8Array, nonceB64: string, wrap
77
77
  *
78
78
  * Post-quantum secure hybrid encryption combining:
79
79
  * - X25519 ECDH (classical, proven security)
80
- * - Kyber ML-KEM-768 (post-quantum, NIST Level 3)
80
+ * - ML-KEM-1024 (post-quantum, FIPS 203, NIST Level 5)
81
81
  *
82
- * Both key exchanges must succeed for decryption, providing
83
- * security against both classical and quantum attacks.
82
+ * v2 (current write format): the content key is wrapped once, under a KEK
83
+ * derived from BOTH shared secrets (HKDF(ss_mlkem || ss_x25519) with
84
+ * transcript binding) — both key exchanges must succeed to decrypt, so
85
+ * breaking either primitive alone is insufficient.
86
+ *
87
+ * v1 (read-only legacy): wrapped the key independently under each
88
+ * primitive; either secret alone sufficed. Kept only for decrypting
89
+ * existing envelopes — see the 2026-07-05 security audit.
84
90
  */
85
91
 
86
92
  interface HybridIdentity {
@@ -116,12 +122,21 @@ interface HybridSecretKeys {
116
122
  kyberSecB64: string;
117
123
  }
118
124
 
119
- type HybridEnvelope = Omit<OmniHybridV1, 'meta'> & {
125
+ type HybridEnvelopeV1 = Omit<OmniHybridV1, 'meta'> & {
120
126
  meta: OmniHybridV1['meta'] & {
121
127
  senderName?: string;
122
128
  senderId?: string;
123
129
  };
124
130
  };
131
+
132
+ type HybridEnvelopeV2 = Omit<OmniHybridV2, 'meta'> & {
133
+ meta: OmniHybridV2['meta'] & {
134
+ senderName?: string;
135
+ senderId?: string;
136
+ };
137
+ };
138
+ /** Any hybrid envelope this module can decrypt. */
139
+ type HybridEnvelope = HybridEnvelopeV1 | HybridEnvelopeV2;
125
140
  /**
126
141
  * Generate a new hybrid identity with both X25519 and Kyber keys.
127
142
  */
@@ -139,7 +154,12 @@ declare function getPublicKeys(identity: HybridIdentity): HybridPublicKeys;
139
154
  */
140
155
  declare function getSecretKeys(identity: HybridIdentity): HybridSecretKeys;
141
156
  /**
142
- * Encrypt a message using hybrid X25519 + Kyber encryption.
157
+ * Encrypt a message using hybrid X25519 + ML-KEM-1024 encryption (v2).
158
+ *
159
+ * The content key is wrapped exactly once, under a KEK derived from both
160
+ * shared secrets together — breaking either primitive alone is insufficient
161
+ * to unwrap. (v1 wrapped the key independently under each primitive, which
162
+ * reduced security to min(X25519, ML-KEM); v1 is now read-only legacy.)
143
163
  *
144
164
  * @param plaintext - Message to encrypt (string or bytes)
145
165
  * @param recipientPublicKeys - Recipient's public keys
@@ -148,12 +168,14 @@ declare function getSecretKeys(identity: HybridIdentity): HybridSecretKeys;
148
168
  declare function hybridEncrypt(plaintext: string | Uint8Array, recipientPublicKeys: HybridPublicKeys, sender?: {
149
169
  name?: string;
150
170
  id?: string;
151
- }): Promise<HybridEnvelope>;
171
+ }): Promise<HybridEnvelopeV2>;
152
172
  /**
153
- * Decrypt a hybrid envelope.
173
+ * Decrypt a hybrid envelope (v2, v1, or legacy pqc-demo).
154
174
  *
155
- * Tries Kyber first (post-quantum), falls back to X25519 (classical).
156
- * Both must be valid for the envelope to be considered secure.
175
+ * v2: the content key is unwrapped from a single AND-combined KEK — both
176
+ * ML-KEM and X25519 secrets are required, and any failure is fatal.
177
+ * v1/legacy: read-only compatibility path (either secret suffices — the
178
+ * defect v2 exists to fix).
157
179
  *
158
180
  * @param envelope - Encrypted envelope
159
181
  * @param secretKeys - Recipient's secret keys
@@ -641,4 +663,4 @@ declare const BOX_KEY_SIZE: number;
641
663
  /** NaCl box nonce size (24 bytes) */
642
664
  declare const BOX_NONCE_SIZE: number;
643
665
 
644
- export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, type Blake3Options, type BoxPayload, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, type ChaChaPayload, type ClassicalWrap, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, type DilithiumKeypair, type DilithiumKeypairB64, type DilithiumSignature, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, type HkdfHash, type HkdfOptions, type HybridEnvelope, type HybridIdentity, type HybridPublicKeys, type HybridSecretKeys, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, type KyberEncapsulation, type KyberKeypair, type KyberKeypairB64, type KyberSuite, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, type SecretboxPayload, type X25519Keypair, type X25519KeypairHex, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
666
+ export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, type Blake3Options, type BoxPayload, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, type ChaChaPayload, type ClassicalWrap, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, type DilithiumKeypair, type DilithiumKeypairB64, type DilithiumSignature, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, type HkdfHash, type HkdfOptions, type HybridEnvelope, type HybridEnvelopeV1, type HybridEnvelopeV2, type HybridIdentity, type HybridPublicKeys, type HybridSecretKeys, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, type KyberEncapsulation, type KyberKeypair, type KyberKeypairB64, type KyberSuite, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, type SecretboxPayload, type X25519Keypair, type X25519KeypairHex, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
@@ -255,8 +255,7 @@ async function loadDilithium() {
255
255
  const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
256
256
  dilithiumModule = ml_dsa65;
257
257
  return dilithiumModule;
258
- } catch (e) {
259
- console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
258
+ } catch {
260
259
  return null;
261
260
  }
262
261
  }
@@ -274,8 +273,7 @@ async function generateDilithiumKeypair() {
274
273
  publicB64: toB64(kp.publicKey),
275
274
  secretB64: toB64(kp.secretKey)
276
275
  };
277
- } catch (e) {
278
- console.warn("[Dilithium] Key generation failed:", e);
276
+ } catch {
279
277
  return null;
280
278
  }
281
279
  }
@@ -344,11 +342,13 @@ var DILITHIUM_SECRET_KEY_SIZE = 4032;
344
342
  var DILITHIUM_SIGNATURE_SIZE = 3309;
345
343
  var DILITHIUM_ALGORITHM = "ML-DSA-65";
346
344
  var ENVELOPE_VERSION = OMNI_VERSIONS.HYBRID_V1;
345
+ var ENVELOPE_VERSION_V2 = OMNI_VERSIONS.HYBRID_V2;
347
346
  var ENVELOPE_VERSION_LEGACY = DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
348
- var ENVELOPE_SUITE = "x25519+kyber768";
347
+ var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
349
348
  var ENVELOPE_AEAD = "xsalsa20poly1305";
350
349
  var SUPPORTED_ENVELOPE_VERSIONS = [
351
350
  ENVELOPE_VERSION,
351
+ ENVELOPE_VERSION_V2,
352
352
  ENVELOPE_VERSION_LEGACY
353
353
  ];
354
354
  var VersionMismatchError = class extends Error {
@@ -380,7 +380,6 @@ async function generateHybridIdentity(name) {
380
380
  const x25519 = generateX25519Keypair();
381
381
  const kyber = await generateKyberKeypair();
382
382
  if (!kyber) {
383
- console.error("Kyber key generation failed - library not available");
384
383
  return null;
385
384
  }
386
385
  return {
@@ -422,49 +421,74 @@ function getSecretKeys(identity) {
422
421
  kyberSecB64: identity.kyberSecB64
423
422
  };
424
423
  }
424
+ function combinedKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
425
+ const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
426
+ ikm.set(kyberShared, 0);
427
+ ikm.set(x25519Shared, kyberShared.length);
428
+ try {
429
+ return hkdfFlex(ikm, "omnituum/hybrid-v2", `wrap-ck|${x25519EpkHex}|${kyberKemCtB64}`);
430
+ } finally {
431
+ ikm.fill(0);
432
+ }
433
+ }
425
434
  async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
426
435
  const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
427
436
  const CK = rand32();
428
- const contentNonce = rand24();
429
- const ciphertext = nacl.secretbox(pt, contentNonce, CK);
430
- const x25519EphKp = nacl.box.keyPair();
431
- const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
432
- const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
433
- const x25519Kek = hkdfFlex(x25519Shared, "omnituum/x25519", "wrap-ck");
434
- const x25519WrapNonce = rand24();
435
- const x25519Wrapped = nacl.secretbox(CK, x25519WrapNonce, x25519Kek);
436
- const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
437
- const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/kyber", "wrap-ck");
438
- const kyberWrapNonce = rand24();
439
- const kyberWrapped = nacl.secretbox(CK, kyberWrapNonce, kyberKek);
440
- return {
441
- v: ENVELOPE_VERSION,
442
- suite: ENVELOPE_SUITE,
443
- aead: ENVELOPE_AEAD,
444
- x25519Epk: toHex(x25519EphKp.publicKey),
445
- x25519Wrap: {
446
- nonce: b64(x25519WrapNonce),
447
- wrapped: b64(x25519Wrapped)
448
- },
449
- kyberKemCt: b64(kyberResult.ciphertext),
450
- kyberWrap: {
451
- nonce: b64(kyberWrapNonce),
452
- wrapped: b64(kyberWrapped)
453
- },
454
- contentNonce: b64(contentNonce),
455
- ciphertext: b64(ciphertext),
456
- meta: {
457
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
458
- senderName: sender?.name,
459
- senderId: sender?.id
460
- }
461
- };
437
+ try {
438
+ const contentNonce = rand24();
439
+ const ciphertext = nacl.secretbox(pt, contentNonce, CK);
440
+ const x25519EphKp = nacl.box.keyPair();
441
+ const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
442
+ const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
443
+ const x25519EpkHex = toHex(x25519EphKp.publicKey);
444
+ const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
445
+ const kyberKemCtB64 = b64(kyberResult.ciphertext);
446
+ const kek = combinedKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
447
+ const ckWrapNonce = rand24();
448
+ const ckWrapped = nacl.secretbox(CK, ckWrapNonce, kek);
449
+ kek.fill(0);
450
+ x25519Shared.fill(0);
451
+ kyberResult.sharedSecret.fill(0);
452
+ x25519EphKp.secretKey.fill(0);
453
+ return {
454
+ v: ENVELOPE_VERSION_V2,
455
+ suite: ENVELOPE_SUITE_V2,
456
+ aead: ENVELOPE_AEAD,
457
+ x25519Epk: x25519EpkHex,
458
+ kyberKemCt: kyberKemCtB64,
459
+ ckWrap: {
460
+ nonce: b64(ckWrapNonce),
461
+ wrapped: b64(ckWrapped)
462
+ },
463
+ contentNonce: b64(contentNonce),
464
+ ciphertext: b64(ciphertext),
465
+ meta: {
466
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
467
+ senderName: sender?.name,
468
+ senderId: sender?.id
469
+ }
470
+ };
471
+ } finally {
472
+ CK.fill(0);
473
+ }
474
+ }
475
+ async function unwrapCkV2(envelope, secretKeys) {
476
+ const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
477
+ const ephPk = fromHex(envelope.x25519Epk);
478
+ const sk = fromHex(secretKeys.x25519SecHex);
479
+ const x25519Shared = nacl.scalarMult(sk, ephPk);
480
+ const kek = combinedKekV2(kyberShared, x25519Shared, envelope.x25519Epk, envelope.kyberKemCt);
481
+ kyberShared.fill(0);
482
+ x25519Shared.fill(0);
483
+ const CK = nacl.secretbox.open(ub64(envelope.ckWrap.wrapped), ub64(envelope.ckWrap.nonce), kek);
484
+ kek.fill(0);
485
+ if (!CK) {
486
+ throw new Error("Could not unwrap content key \u2014 combined-KEK authentication failed");
487
+ }
488
+ return CK;
462
489
  }
463
- async function hybridDecrypt(envelope, secretKeys) {
464
- const version = envelope.v;
465
- assertEnvelopeVersion(version);
490
+ async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
466
491
  let CK = null;
467
- const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
468
492
  try {
469
493
  const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
470
494
  const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
@@ -473,11 +497,7 @@ async function hybridDecrypt(envelope, secretKeys) {
473
497
  ub64(envelope.kyberWrap.nonce),
474
498
  kyberKek
475
499
  );
476
- if (CK) {
477
- console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
478
- }
479
- } catch (e) {
480
- console.warn("[Hybrid] Kyber decapsulation failed:", e);
500
+ } catch {
481
501
  }
482
502
  if (!CK) {
483
503
  try {
@@ -490,21 +510,28 @@ async function hybridDecrypt(envelope, secretKeys) {
490
510
  ub64(envelope.x25519Wrap.nonce),
491
511
  x25519Kek
492
512
  );
493
- if (CK) {
494
- console.log("[Hybrid] Decrypted using X25519 (classical)");
495
- }
496
- } catch (e) {
497
- console.warn("[Hybrid] X25519 decryption failed:", e);
513
+ } catch {
498
514
  }
499
515
  }
500
516
  if (!CK) {
501
517
  throw new Error("Could not unwrap content key with either algorithm");
502
518
  }
519
+ return CK;
520
+ }
521
+ async function hybridDecrypt(envelope, secretKeys) {
522
+ const version = envelope.v;
523
+ assertEnvelopeVersion(version);
524
+ const CK = version === ENVELOPE_VERSION_V2 ? await unwrapCkV2(envelope, secretKeys) : await unwrapCkV1(
525
+ envelope,
526
+ secretKeys,
527
+ version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum"
528
+ );
503
529
  const pt = nacl.secretbox.open(
504
530
  ub64(envelope.ciphertext),
505
531
  ub64(envelope.contentNonce),
506
532
  CK
507
533
  );
534
+ CK.fill(0);
508
535
  if (!pt) {
509
536
  throw new Error("Content authentication failed");
510
537
  }