@omnituum/pqc-shared 0.4.0 → 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 (40) hide show
  1. package/dist/crypto/index.cjs +91 -54
  2. package/dist/crypto/index.d.cts +43 -11
  3. package/dist/crypto/index.d.ts +43 -11
  4. package/dist/crypto/index.js +87 -55
  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 +368 -229
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +362 -230
  15. package/dist/{integrity-ByMp24VA.d.cts → integrity-BkBDrHA-.d.cts} +12 -4
  16. package/dist/{integrity-BPvNsC50.d.ts → integrity-DsFJv5c-.d.ts} +12 -4
  17. package/dist/{types-Dx_AFqow.d.cts → types-DmnVueAY.d.cts} +15 -5
  18. package/dist/{types-Dx_AFqow.d.ts → types-DmnVueAY.d.ts} +15 -5
  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 +19 -25
  28. package/src/crypto/dilithium.ts +4 -4
  29. package/src/crypto/hybrid.ts +182 -80
  30. package/src/crypto/index.ts +8 -1
  31. package/src/crypto/kyber.ts +20 -0
  32. package/src/fs/decrypt.ts +145 -68
  33. package/src/fs/encrypt.ts +161 -112
  34. package/src/fs/format.ts +110 -15
  35. package/src/fs/index.ts +4 -0
  36. package/src/fs/types.ts +77 -6
  37. package/src/index.ts +9 -0
  38. package/src/utils/integrity.ts +16 -15
  39. package/src/vault/manager.ts +1 -1
  40. package/src/version.ts +29 -7
@@ -195,6 +195,11 @@ function deriveKeyFromShared(shared, salt, info) {
195
195
  return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
196
196
  }
197
197
  var KYBER_SUITE = "ML-KEM-1024-FIPS203";
198
+ var KYBER_PUBLIC_KEY_SIZE = 1568;
199
+ var KYBER_SECRET_KEY_SIZE = 3168;
200
+ var KYBER_CIPHERTEXT_SIZE = 1568;
201
+ var KYBER_SHARED_SECRET_SIZE = 32;
202
+ var KYBER_SEED_SIZE = 64;
198
203
  async function isKyberAvailable() {
199
204
  return true;
200
205
  }
@@ -250,8 +255,7 @@ async function loadDilithium() {
250
255
  const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
251
256
  dilithiumModule = ml_dsa65;
252
257
  return dilithiumModule;
253
- } catch (e) {
254
- console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
258
+ } catch {
255
259
  return null;
256
260
  }
257
261
  }
@@ -269,8 +273,7 @@ async function generateDilithiumKeypair() {
269
273
  publicB64: toB64(kp.publicKey),
270
274
  secretB64: toB64(kp.secretKey)
271
275
  };
272
- } catch (e) {
273
- console.warn("[Dilithium] Key generation failed:", e);
276
+ } catch {
274
277
  return null;
275
278
  }
276
279
  }
@@ -339,11 +342,13 @@ var DILITHIUM_SECRET_KEY_SIZE = 4032;
339
342
  var DILITHIUM_SIGNATURE_SIZE = 3309;
340
343
  var DILITHIUM_ALGORITHM = "ML-DSA-65";
341
344
  var ENVELOPE_VERSION = OMNI_VERSIONS.HYBRID_V1;
345
+ var ENVELOPE_VERSION_V2 = OMNI_VERSIONS.HYBRID_V2;
342
346
  var ENVELOPE_VERSION_LEGACY = DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
343
- var ENVELOPE_SUITE = "x25519+kyber768";
347
+ var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
344
348
  var ENVELOPE_AEAD = "xsalsa20poly1305";
345
349
  var SUPPORTED_ENVELOPE_VERSIONS = [
346
350
  ENVELOPE_VERSION,
351
+ ENVELOPE_VERSION_V2,
347
352
  ENVELOPE_VERSION_LEGACY
348
353
  ];
349
354
  var VersionMismatchError = class extends Error {
@@ -375,7 +380,6 @@ async function generateHybridIdentity(name) {
375
380
  const x25519 = generateX25519Keypair();
376
381
  const kyber = await generateKyberKeypair();
377
382
  if (!kyber) {
378
- console.error("Kyber key generation failed - library not available");
379
383
  return null;
380
384
  }
381
385
  return {
@@ -417,49 +421,74 @@ function getSecretKeys(identity) {
417
421
  kyberSecB64: identity.kyberSecB64
418
422
  };
419
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
+ }
420
434
  async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
421
435
  const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
422
436
  const CK = rand32();
423
- const contentNonce = rand24();
424
- const ciphertext = nacl.secretbox(pt, contentNonce, CK);
425
- const x25519EphKp = nacl.box.keyPair();
426
- const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
427
- const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
428
- const x25519Kek = hkdfFlex(x25519Shared, "omnituum/x25519", "wrap-ck");
429
- const x25519WrapNonce = rand24();
430
- const x25519Wrapped = nacl.secretbox(CK, x25519WrapNonce, x25519Kek);
431
- const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
432
- const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/kyber", "wrap-ck");
433
- const kyberWrapNonce = rand24();
434
- const kyberWrapped = nacl.secretbox(CK, kyberWrapNonce, kyberKek);
435
- return {
436
- v: ENVELOPE_VERSION,
437
- suite: ENVELOPE_SUITE,
438
- aead: ENVELOPE_AEAD,
439
- x25519Epk: toHex(x25519EphKp.publicKey),
440
- x25519Wrap: {
441
- nonce: b64(x25519WrapNonce),
442
- wrapped: b64(x25519Wrapped)
443
- },
444
- kyberKemCt: b64(kyberResult.ciphertext),
445
- kyberWrap: {
446
- nonce: b64(kyberWrapNonce),
447
- wrapped: b64(kyberWrapped)
448
- },
449
- contentNonce: b64(contentNonce),
450
- ciphertext: b64(ciphertext),
451
- meta: {
452
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
453
- senderName: sender?.name,
454
- senderId: sender?.id
455
- }
456
- };
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;
457
489
  }
458
- async function hybridDecrypt(envelope, secretKeys) {
459
- const version = envelope.v;
460
- assertEnvelopeVersion(version);
490
+ async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
461
491
  let CK = null;
462
- const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
463
492
  try {
464
493
  const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
465
494
  const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
@@ -468,11 +497,7 @@ async function hybridDecrypt(envelope, secretKeys) {
468
497
  ub64(envelope.kyberWrap.nonce),
469
498
  kyberKek
470
499
  );
471
- if (CK) {
472
- console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
473
- }
474
- } catch (e) {
475
- console.warn("[Hybrid] Kyber decapsulation failed:", e);
500
+ } catch {
476
501
  }
477
502
  if (!CK) {
478
503
  try {
@@ -485,21 +510,28 @@ async function hybridDecrypt(envelope, secretKeys) {
485
510
  ub64(envelope.x25519Wrap.nonce),
486
511
  x25519Kek
487
512
  );
488
- if (CK) {
489
- console.log("[Hybrid] Decrypted using X25519 (classical)");
490
- }
491
- } catch (e) {
492
- console.warn("[Hybrid] X25519 decryption failed:", e);
513
+ } catch {
493
514
  }
494
515
  }
495
516
  if (!CK) {
496
517
  throw new Error("Could not unwrap content key with either algorithm");
497
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
+ );
498
529
  const pt = nacl.secretbox.open(
499
530
  ub64(envelope.ciphertext),
500
531
  ub64(envelope.contentNonce),
501
532
  CK
502
533
  );
534
+ CK.fill(0);
503
535
  if (!pt) {
504
536
  throw new Error("Content authentication failed");
505
537
  }
@@ -668,4 +700,4 @@ var HKDF_SHA512_OUTPUT_SIZE = 64;
668
700
  var HKDF_SHA256_MAX_OUTPUT = 255 * 32;
669
701
  var HKDF_SHA512_MAX_OUTPUT = 255 * 64;
670
702
 
671
- export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, KYBER_SUITE, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, 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 };
703
+ export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, 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 };
@@ -8,16 +8,43 @@
8
8
  */
9
9
  /** Magic bytes: "OQEF" (Omnituum Quantum Encrypted File) */
10
10
  declare const OQE_MAGIC: Uint8Array<ArrayBuffer>;
11
- /** Current format version */
12
- declare const OQE_FORMAT_VERSION = 1;
11
+ /**
12
+ * OQE format versions.
13
+ *
14
+ * v1 (LEGACY, read-only): a single AES-GCM IV was reused for both the
15
+ * metadata section and the content section under the same content key —
16
+ * catastrophic GCM nonce reuse — and hybrid mode wrapped the content key
17
+ * independently under X25519 and Kyber (either secret alone unwrapped it).
18
+ * Kept only so pre-existing files remain decryptable.
19
+ *
20
+ * v2 (CURRENT, write format): distinct random IVs per AES-GCM section, the
21
+ * serialized header bound as AEAD associated data, and hybrid mode wraps the
22
+ * content key once under an AND-combined KEK (HKDF(ss_mlkem || ss_x25519)
23
+ * with transcript binding) — both primitives must be broken to unwrap.
24
+ * See the 2026-07-06 fs security fix.
25
+ */
26
+ declare const OQE_FORMAT_VERSION_V1 = 1;
27
+ declare const OQE_FORMAT_VERSION_V2 = 2;
28
+ /** Current (write) format version. */
29
+ declare const OQE_FORMAT_VERSION = 2;
30
+ /** Format versions this library can read. */
31
+ declare const SUPPORTED_OQE_VERSIONS: readonly [1, 2];
13
32
  /** Supported encryption modes */
14
33
  type OQEMode = 'hybrid' | 'password';
15
34
  /** Algorithm suite identifiers */
16
35
  declare const ALGORITHM_SUITES: {
17
- /** Hybrid: X25519 ECDH + Kyber768 KEM + AES-256-GCM */
36
+ /**
37
+ * LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
38
+ * wraps (either secret unwraps). Only appears in v1 files. Never written.
39
+ */
18
40
  readonly HYBRID_X25519_KYBER768_AES256GCM: 1;
19
41
  /** Password: Argon2id + AES-256-GCM */
20
42
  readonly PASSWORD_ARGON2ID_AES256GCM: 2;
43
+ /**
44
+ * Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
45
+ * (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
46
+ */
47
+ readonly HYBRID_X25519_MLKEM1024_AES256GCM: 3;
21
48
  };
22
49
  type AlgorithmSuiteId = typeof ALGORITHM_SUITES[keyof typeof ALGORITHM_SUITES];
23
50
  interface Argon2idParams {
@@ -79,10 +106,25 @@ interface OQEHeader {
79
106
  metadataLength: number;
80
107
  /** Length of key material section */
81
108
  keyMaterialLength: number;
82
- /** AES-GCM initialization vector */
109
+ /**
110
+ * AES-GCM IV for the metadata section. In v1 this same IV was (incorrectly)
111
+ * also used for the content section; v2 uses `contentIv` for content.
112
+ */
83
113
  iv: Uint8Array;
114
+ /**
115
+ * AES-GCM IV for the content section (v2 only). Distinct from `iv` so the
116
+ * two sections never share a (key, nonce) pair. Undefined for v1 files.
117
+ */
118
+ contentIv?: Uint8Array;
84
119
  }
85
- /** Fixed header size in bytes (before variable-length sections) */
120
+ /** Fixed v1 header size in bytes (before variable-length sections). */
121
+ declare const OQE_HEADER_SIZE_V1 = 30;
122
+ /** Fixed v2 header size: v1 layout plus a 12-byte content IV. */
123
+ declare const OQE_HEADER_SIZE_V2 = 42;
124
+ /**
125
+ * @deprecated Use OQE_HEADER_SIZE_V1 / OQE_HEADER_SIZE_V2. Retained as the v1
126
+ * size for source compatibility.
127
+ */
86
128
  declare const OQE_HEADER_SIZE = 30;
87
129
  /**
88
130
  * Hybrid Mode Key Material:
@@ -107,6 +149,30 @@ interface HybridKeyMaterial {
107
149
  /** Kyber wrap nonce */
108
150
  kyberNonce: Uint8Array;
109
151
  }
152
+ /**
153
+ * v2 Hybrid Mode Key Material — single AND-combined wrap.
154
+ *
155
+ * The content key is wrapped exactly once under a KEK derived from BOTH shared
156
+ * secrets: HKDF(ss_mlkem || ss_x25519) with the ephemeral X25519 key and the
157
+ * Kyber ciphertext bound into the info string. Both the X25519 and ML-KEM
158
+ * exchanges must succeed to unwrap — there is no per-primitive fallback.
159
+ *
160
+ * Serialized layout:
161
+ * - X25519 ephemeral public key (32 bytes)
162
+ * - Kyber KEM ciphertext (2-byte length prefix + data)
163
+ * - Content-key wrap nonce (24 bytes)
164
+ * - Wrapped content key (2-byte length prefix + data, 48 bytes)
165
+ */
166
+ interface HybridKeyMaterialV2 {
167
+ /** X25519 ephemeral public key */
168
+ x25519EphemeralPk: Uint8Array;
169
+ /** Kyber KEM ciphertext */
170
+ kyberCiphertext: Uint8Array;
171
+ /** Wrap nonce for the single content-key wrap (NaCl secretbox) */
172
+ ckWrapNonce: Uint8Array;
173
+ /** Content key wrapped under the AND-combined KEK */
174
+ ckWrapped: Uint8Array;
175
+ }
110
176
  /**
111
177
  * Password Mode Key Material:
112
178
  * - Argon2id salt (32 bytes)
@@ -209,6 +275,14 @@ declare function toUint8Array(input: FileInput): Promise<Uint8Array>;
209
275
  * Outputs .oqe (Omnituum Quantum Encrypted) files.
210
276
  */
211
277
 
278
+ /**
279
+ * Derive the v2 AND-combined KEK for file encryption. Requires BOTH shared
280
+ * secrets; the envelope's own KEM values are bound into the HKDF info so a
281
+ * spliced ephemeral key or Kyber ciphertext derives a different KEK and the
282
+ * wrap fails authentication (X-Wing-style transcript binding). Domain-separated
283
+ * from the message-envelope KEK via the "omnituum/fs/hybrid-v2" salt.
284
+ */
285
+ declare function combinedFileKekV2(kyberShared: Uint8Array, x25519Shared: Uint8Array, x25519EpkHex: string, kyberKemCtB64: string): Uint8Array;
212
286
  interface EncryptFileInput {
213
287
  /** File data */
214
288
  data: FileInput;
@@ -318,4 +392,4 @@ interface OQEFileInfo {
318
392
  */
319
393
  declare function inspectOQEFile(data: FileInput): Promise<OQEFileInfo>;
320
394
 
321
- export { type Argon2idParams as A, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, type HybridKeyMaterial as H, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type OQEMetadata as g, type AlgorithmSuiteId as h, DEFAULT_ARGON2ID_PARAMS as i, encryptFileForSelf as j, decryptFileForSelf as k, inspectOQEFile as l, type OQEMode as m, type HybridEncryptOptions as n, type PasswordEncryptOptions as o, type HybridDecryptOptions as p, type PasswordDecryptOptions as q, type OQEErrorCode as r, type ProgressCallback as s, OQEError as t, OQE_MAGIC as u, OQE_FORMAT_VERSION as v, ALGORITHM_SUITES as w, OQE_HEADER_SIZE as x, toUint8Array as y };
395
+ export { type Argon2idParams as A, ALGORITHM_SUITES as B, OQE_HEADER_SIZE_V1 as C, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, OQE_HEADER_SIZE_V2 as G, type HybridKeyMaterial as H, OQE_HEADER_SIZE as I, toUint8Array as J, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, SUPPORTED_OQE_VERSIONS as S, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type HybridKeyMaterialV2 as g, type OQEMetadata as h, type AlgorithmSuiteId as i, DEFAULT_ARGON2ID_PARAMS as j, encryptFileForSelf as k, combinedFileKekV2 as l, decryptFileForSelf as m, inspectOQEFile as n, type OQEMode as o, type HybridEncryptOptions as p, type PasswordEncryptOptions as q, type HybridDecryptOptions as r, type PasswordDecryptOptions as s, type OQEErrorCode as t, type ProgressCallback as u, OQEError as v, OQE_MAGIC as w, OQE_FORMAT_VERSION_V1 as x, OQE_FORMAT_VERSION_V2 as y, OQE_FORMAT_VERSION as z };
@@ -8,16 +8,43 @@
8
8
  */
9
9
  /** Magic bytes: "OQEF" (Omnituum Quantum Encrypted File) */
10
10
  declare const OQE_MAGIC: Uint8Array<ArrayBuffer>;
11
- /** Current format version */
12
- declare const OQE_FORMAT_VERSION = 1;
11
+ /**
12
+ * OQE format versions.
13
+ *
14
+ * v1 (LEGACY, read-only): a single AES-GCM IV was reused for both the
15
+ * metadata section and the content section under the same content key —
16
+ * catastrophic GCM nonce reuse — and hybrid mode wrapped the content key
17
+ * independently under X25519 and Kyber (either secret alone unwrapped it).
18
+ * Kept only so pre-existing files remain decryptable.
19
+ *
20
+ * v2 (CURRENT, write format): distinct random IVs per AES-GCM section, the
21
+ * serialized header bound as AEAD associated data, and hybrid mode wraps the
22
+ * content key once under an AND-combined KEK (HKDF(ss_mlkem || ss_x25519)
23
+ * with transcript binding) — both primitives must be broken to unwrap.
24
+ * See the 2026-07-06 fs security fix.
25
+ */
26
+ declare const OQE_FORMAT_VERSION_V1 = 1;
27
+ declare const OQE_FORMAT_VERSION_V2 = 2;
28
+ /** Current (write) format version. */
29
+ declare const OQE_FORMAT_VERSION = 2;
30
+ /** Format versions this library can read. */
31
+ declare const SUPPORTED_OQE_VERSIONS: readonly [1, 2];
13
32
  /** Supported encryption modes */
14
33
  type OQEMode = 'hybrid' | 'password';
15
34
  /** Algorithm suite identifiers */
16
35
  declare const ALGORITHM_SUITES: {
17
- /** Hybrid: X25519 ECDH + Kyber768 KEM + AES-256-GCM */
36
+ /**
37
+ * LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
38
+ * wraps (either secret unwraps). Only appears in v1 files. Never written.
39
+ */
18
40
  readonly HYBRID_X25519_KYBER768_AES256GCM: 1;
19
41
  /** Password: Argon2id + AES-256-GCM */
20
42
  readonly PASSWORD_ARGON2ID_AES256GCM: 2;
43
+ /**
44
+ * Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
45
+ * (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
46
+ */
47
+ readonly HYBRID_X25519_MLKEM1024_AES256GCM: 3;
21
48
  };
22
49
  type AlgorithmSuiteId = typeof ALGORITHM_SUITES[keyof typeof ALGORITHM_SUITES];
23
50
  interface Argon2idParams {
@@ -79,10 +106,25 @@ interface OQEHeader {
79
106
  metadataLength: number;
80
107
  /** Length of key material section */
81
108
  keyMaterialLength: number;
82
- /** AES-GCM initialization vector */
109
+ /**
110
+ * AES-GCM IV for the metadata section. In v1 this same IV was (incorrectly)
111
+ * also used for the content section; v2 uses `contentIv` for content.
112
+ */
83
113
  iv: Uint8Array;
114
+ /**
115
+ * AES-GCM IV for the content section (v2 only). Distinct from `iv` so the
116
+ * two sections never share a (key, nonce) pair. Undefined for v1 files.
117
+ */
118
+ contentIv?: Uint8Array;
84
119
  }
85
- /** Fixed header size in bytes (before variable-length sections) */
120
+ /** Fixed v1 header size in bytes (before variable-length sections). */
121
+ declare const OQE_HEADER_SIZE_V1 = 30;
122
+ /** Fixed v2 header size: v1 layout plus a 12-byte content IV. */
123
+ declare const OQE_HEADER_SIZE_V2 = 42;
124
+ /**
125
+ * @deprecated Use OQE_HEADER_SIZE_V1 / OQE_HEADER_SIZE_V2. Retained as the v1
126
+ * size for source compatibility.
127
+ */
86
128
  declare const OQE_HEADER_SIZE = 30;
87
129
  /**
88
130
  * Hybrid Mode Key Material:
@@ -107,6 +149,30 @@ interface HybridKeyMaterial {
107
149
  /** Kyber wrap nonce */
108
150
  kyberNonce: Uint8Array;
109
151
  }
152
+ /**
153
+ * v2 Hybrid Mode Key Material — single AND-combined wrap.
154
+ *
155
+ * The content key is wrapped exactly once under a KEK derived from BOTH shared
156
+ * secrets: HKDF(ss_mlkem || ss_x25519) with the ephemeral X25519 key and the
157
+ * Kyber ciphertext bound into the info string. Both the X25519 and ML-KEM
158
+ * exchanges must succeed to unwrap — there is no per-primitive fallback.
159
+ *
160
+ * Serialized layout:
161
+ * - X25519 ephemeral public key (32 bytes)
162
+ * - Kyber KEM ciphertext (2-byte length prefix + data)
163
+ * - Content-key wrap nonce (24 bytes)
164
+ * - Wrapped content key (2-byte length prefix + data, 48 bytes)
165
+ */
166
+ interface HybridKeyMaterialV2 {
167
+ /** X25519 ephemeral public key */
168
+ x25519EphemeralPk: Uint8Array;
169
+ /** Kyber KEM ciphertext */
170
+ kyberCiphertext: Uint8Array;
171
+ /** Wrap nonce for the single content-key wrap (NaCl secretbox) */
172
+ ckWrapNonce: Uint8Array;
173
+ /** Content key wrapped under the AND-combined KEK */
174
+ ckWrapped: Uint8Array;
175
+ }
110
176
  /**
111
177
  * Password Mode Key Material:
112
178
  * - Argon2id salt (32 bytes)
@@ -209,6 +275,14 @@ declare function toUint8Array(input: FileInput): Promise<Uint8Array>;
209
275
  * Outputs .oqe (Omnituum Quantum Encrypted) files.
210
276
  */
211
277
 
278
+ /**
279
+ * Derive the v2 AND-combined KEK for file encryption. Requires BOTH shared
280
+ * secrets; the envelope's own KEM values are bound into the HKDF info so a
281
+ * spliced ephemeral key or Kyber ciphertext derives a different KEK and the
282
+ * wrap fails authentication (X-Wing-style transcript binding). Domain-separated
283
+ * from the message-envelope KEK via the "omnituum/fs/hybrid-v2" salt.
284
+ */
285
+ declare function combinedFileKekV2(kyberShared: Uint8Array, x25519Shared: Uint8Array, x25519EpkHex: string, kyberKemCtB64: string): Uint8Array;
212
286
  interface EncryptFileInput {
213
287
  /** File data */
214
288
  data: FileInput;
@@ -318,4 +392,4 @@ interface OQEFileInfo {
318
392
  */
319
393
  declare function inspectOQEFile(data: FileInput): Promise<OQEFileInfo>;
320
394
 
321
- export { type Argon2idParams as A, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, type HybridKeyMaterial as H, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type OQEMetadata as g, type AlgorithmSuiteId as h, DEFAULT_ARGON2ID_PARAMS as i, encryptFileForSelf as j, decryptFileForSelf as k, inspectOQEFile as l, type OQEMode as m, type HybridEncryptOptions as n, type PasswordEncryptOptions as o, type HybridDecryptOptions as p, type PasswordDecryptOptions as q, type OQEErrorCode as r, type ProgressCallback as s, OQEError as t, OQE_MAGIC as u, OQE_FORMAT_VERSION as v, ALGORITHM_SUITES as w, OQE_HEADER_SIZE as x, toUint8Array as y };
395
+ export { type Argon2idParams as A, ALGORITHM_SUITES as B, OQE_HEADER_SIZE_V1 as C, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, OQE_HEADER_SIZE_V2 as G, type HybridKeyMaterial as H, OQE_HEADER_SIZE as I, toUint8Array as J, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, SUPPORTED_OQE_VERSIONS as S, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type HybridKeyMaterialV2 as g, type OQEMetadata as h, type AlgorithmSuiteId as i, DEFAULT_ARGON2ID_PARAMS as j, encryptFileForSelf as k, combinedFileKekV2 as l, decryptFileForSelf as m, inspectOQEFile as n, type OQEMode as o, type HybridEncryptOptions as p, type PasswordEncryptOptions as q, type HybridDecryptOptions as r, type PasswordDecryptOptions as s, type OQEErrorCode as t, type ProgressCallback as u, OQEError as v, OQE_MAGIC as w, OQE_FORMAT_VERSION_V1 as x, OQE_FORMAT_VERSION_V2 as y, OQE_FORMAT_VERSION as z };