@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
package/dist/fs/index.cjs CHANGED
@@ -3,21 +3,32 @@
3
3
  var hashWasm = require('hash-wasm');
4
4
  var sha2_js = require('@noble/hashes/sha2.js');
5
5
  var hmac_js = require('@noble/hashes/hmac.js');
6
- var nacl2 = require('tweetnacl');
6
+ var nacl3 = require('tweetnacl');
7
7
  var mlKem_js = require('@noble/post-quantum/ml-kem.js');
8
8
 
9
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
11
- var nacl2__default = /*#__PURE__*/_interopDefault(nacl2);
11
+ var nacl3__default = /*#__PURE__*/_interopDefault(nacl3);
12
12
 
13
13
  // src/fs/types.ts
14
14
  var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
15
- var OQE_FORMAT_VERSION = 1;
15
+ var OQE_FORMAT_VERSION_V1 = 1;
16
+ var OQE_FORMAT_VERSION_V2 = 2;
17
+ var OQE_FORMAT_VERSION = OQE_FORMAT_VERSION_V2;
18
+ var SUPPORTED_OQE_VERSIONS = [OQE_FORMAT_VERSION_V1, OQE_FORMAT_VERSION_V2];
16
19
  var ALGORITHM_SUITES = {
17
- /** Hybrid: X25519 ECDH + Kyber768 KEM + AES-256-GCM */
20
+ /**
21
+ * LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
22
+ * wraps (either secret unwraps). Only appears in v1 files. Never written.
23
+ */
18
24
  HYBRID_X25519_KYBER768_AES256GCM: 1,
19
25
  /** Password: Argon2id + AES-256-GCM */
20
- PASSWORD_ARGON2ID_AES256GCM: 2
26
+ PASSWORD_ARGON2ID_AES256GCM: 2,
27
+ /**
28
+ * Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
29
+ * (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
30
+ */
31
+ HYBRID_X25519_MLKEM1024_AES256GCM: 3
21
32
  };
22
33
  var DEFAULT_ARGON2ID_PARAMS = {
23
34
  memoryCost: 65536,
@@ -35,7 +46,9 @@ var MIN_ARGON2ID_PARAMS = {
35
46
  hashLength: 32,
36
47
  saltLength: 32
37
48
  };
38
- var OQE_HEADER_SIZE = 30;
49
+ var OQE_HEADER_SIZE_V1 = 30;
50
+ var OQE_HEADER_SIZE_V2 = 42;
51
+ var OQE_HEADER_SIZE = OQE_HEADER_SIZE_V1;
39
52
  var OQEError = class extends Error {
40
53
  constructor(code, message) {
41
54
  super(message);
@@ -121,6 +134,7 @@ function hkdfSha256(ikm, opts) {
121
134
  }
122
135
  return out;
123
136
  }
137
+ var b64 = toB64;
124
138
  var ub64 = fromB64;
125
139
  var u8 = (s) => typeof s === "string" ? textEncoder.encode(s) : s;
126
140
 
@@ -350,6 +364,9 @@ async function aesDecryptStreaming(encrypted, key, onProgress) {
350
364
  }
351
365
 
352
366
  // src/fs/format.ts
367
+ function oqeHeaderSize(version) {
368
+ return version === OQE_FORMAT_VERSION_V2 ? OQE_HEADER_SIZE_V2 : OQE_HEADER_SIZE_V1;
369
+ }
353
370
  function writeUint32BE(value) {
354
371
  const buffer = new ArrayBuffer(4);
355
372
  new DataView(buffer).setUint32(0, value, false);
@@ -367,7 +384,8 @@ function readUint16BE(data, offset) {
367
384
  return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
368
385
  }
369
386
  function writeOQEHeader(header) {
370
- const buffer = new Uint8Array(OQE_HEADER_SIZE);
387
+ const isV2 = header.version === OQE_FORMAT_VERSION_V2;
388
+ const buffer = new Uint8Array(oqeHeaderSize(header.version));
371
389
  let offset = 0;
372
390
  buffer.set(OQE_MAGIC, offset);
373
391
  offset += 4;
@@ -380,11 +398,18 @@ function writeOQEHeader(header) {
380
398
  buffer.set(writeUint32BE(header.keyMaterialLength), offset);
381
399
  offset += 4;
382
400
  buffer.set(header.iv, offset);
401
+ offset += 12;
402
+ if (isV2) {
403
+ if (!header.contentIv || header.contentIv.length !== 12) {
404
+ throw new OQEError("INVALID_HEADER", "v2 header requires a 12-byte contentIv");
405
+ }
406
+ buffer.set(header.contentIv, offset);
407
+ }
383
408
  return buffer;
384
409
  }
385
410
  function parseOQEHeader(data) {
386
- if (data.length < OQE_HEADER_SIZE) {
387
- throw new OQEError("INVALID_HEADER", `File too small: need ${OQE_HEADER_SIZE} bytes, got ${data.length}`);
411
+ if (data.length < OQE_HEADER_SIZE_V1) {
412
+ throw new OQEError("INVALID_HEADER", `File too small: need at least ${OQE_HEADER_SIZE_V1} bytes, got ${data.length}`);
388
413
  }
389
414
  let offset = 0;
390
415
  const magic = data.slice(0, 4);
@@ -393,14 +418,19 @@ function parseOQEHeader(data) {
393
418
  }
394
419
  offset += 4;
395
420
  const version = data[offset++];
396
- if (version !== OQE_FORMAT_VERSION) {
421
+ if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
397
422
  throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
398
423
  }
399
424
  const algorithmSuiteRaw = data[offset++];
400
- if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
425
+ if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
401
426
  throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
402
427
  }
403
428
  const algorithmSuite = algorithmSuiteRaw;
429
+ const isV2 = version === OQE_FORMAT_VERSION_V2;
430
+ const headerSize = oqeHeaderSize(version);
431
+ if (data.length < headerSize) {
432
+ throw new OQEError("INVALID_HEADER", `Truncated v${version} header: need ${headerSize} bytes, got ${data.length}`);
433
+ }
404
434
  const flags = readUint32BE(data, offset);
405
435
  offset += 4;
406
436
  const metadataLength = readUint32BE(data, offset);
@@ -408,13 +438,16 @@ function parseOQEHeader(data) {
408
438
  const keyMaterialLength = readUint32BE(data, offset);
409
439
  offset += 4;
410
440
  const iv = data.slice(offset, offset + 12);
441
+ offset += 12;
442
+ const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
411
443
  return {
412
444
  version,
413
445
  algorithmSuite,
414
446
  flags,
415
447
  metadataLength,
416
448
  keyMaterialLength,
417
- iv
449
+ iv,
450
+ contentIv
418
451
  };
419
452
  }
420
453
  function serializeHybridKeyMaterial(km) {
@@ -465,6 +498,38 @@ function parseHybridKeyMaterial(data) {
465
498
  kyberWrappedKey
466
499
  };
467
500
  }
501
+ function serializeHybridKeyMaterialV2(km) {
502
+ const parts = [];
503
+ parts.push(km.x25519EphemeralPk);
504
+ parts.push(writeUint16BE(km.kyberCiphertext.length));
505
+ parts.push(km.kyberCiphertext);
506
+ parts.push(km.ckWrapNonce);
507
+ parts.push(writeUint16BE(km.ckWrapped.length));
508
+ parts.push(km.ckWrapped);
509
+ const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
510
+ const result = new Uint8Array(totalLength);
511
+ let offset = 0;
512
+ for (const part of parts) {
513
+ result.set(part, offset);
514
+ offset += part.length;
515
+ }
516
+ return result;
517
+ }
518
+ function parseHybridKeyMaterialV2(data) {
519
+ let offset = 0;
520
+ const x25519EphemeralPk = data.slice(offset, offset + 32);
521
+ offset += 32;
522
+ const kyberCtLen = readUint16BE(data, offset);
523
+ offset += 2;
524
+ const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
525
+ offset += kyberCtLen;
526
+ const ckWrapNonce = data.slice(offset, offset + 24);
527
+ offset += 24;
528
+ const ckWrappedLen = readUint16BE(data, offset);
529
+ offset += 2;
530
+ const ckWrapped = data.slice(offset, offset + ckWrappedLen);
531
+ return { x25519EphemeralPk, kyberCiphertext, ckWrapNonce, ckWrapped };
532
+ }
468
533
  function serializePasswordKeyMaterial(km) {
469
534
  const result = new Uint8Array(32 + 4 + 4 + 4);
470
535
  result.set(km.salt, 0);
@@ -506,7 +571,7 @@ function assembleOQEFile(components) {
506
571
  }
507
572
  function parseOQEFile(data) {
508
573
  const header = parseOQEHeader(data);
509
- let offset = OQE_HEADER_SIZE;
574
+ let offset = oqeHeaderSize(header.version);
510
575
  const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
511
576
  offset += header.keyMaterialLength;
512
577
  const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
@@ -541,8 +606,11 @@ function isOQEFile(filenameOrData) {
541
606
  }
542
607
  var OQE_MIME_TYPE = "application/x-omnituum-encrypted";
543
608
  function getAlgorithmName(suiteId) {
609
+ if (suiteId === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
610
+ return "Hybrid (X25519 + ML-KEM-1024 + AES-256-GCM)";
611
+ }
544
612
  if (suiteId === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
545
- return "Hybrid (X25519 + Kyber768 + AES-256-GCM)";
613
+ return "Hybrid legacy (X25519 + Kyber + AES-256-GCM, either-key)";
546
614
  }
547
615
  if (suiteId === ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
548
616
  return "Password (Argon2id + AES-256-GCM)";
@@ -567,61 +635,81 @@ async function kyberDecapsulate(kemCiphertextB64, secretKeyB64) {
567
635
  }
568
636
 
569
637
  // src/fs/encrypt.ts
570
- function hkdfFlex(ikm, salt, info) {
571
- return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
572
- }
638
+ var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
573
639
  function computeIdentityHash(publicKeyHex) {
574
640
  const hash = sha256(fromHex(publicKeyHex));
575
641
  return toHex(hash).slice(0, 16);
576
642
  }
643
+ function combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
644
+ const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
645
+ ikm.set(kyberShared, 0);
646
+ ikm.set(x25519Shared, kyberShared.length);
647
+ try {
648
+ return hkdfSha256(ikm, {
649
+ salt: u8("omnituum/fs/hybrid-v2"),
650
+ info: u8(`wrap-content-key|${x25519EpkHex}|${kyberKemCtB64}`),
651
+ length: 32
652
+ });
653
+ } finally {
654
+ ikm.fill(0);
655
+ }
656
+ }
577
657
  async function encryptHybrid(plaintext, metadata, options) {
578
658
  if (!await isKyberAvailable()) {
579
659
  throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
580
660
  }
581
661
  const contentKey = rand32();
582
- const iv = rand12();
583
- const x25519EphKp = nacl2__default.default.box.keyPair();
584
- const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
585
- const x25519Shared = nacl2__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
586
- const x25519Kek = hkdfFlex(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
587
- const x25519Nonce = rand24();
588
- const x25519WrappedKey = nacl2__default.default.secretbox(contentKey, x25519Nonce, x25519Kek);
589
- const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
590
- const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/fs/kyber", "wrap-content-key");
591
- const kyberNonce = rand24();
592
- const kyberWrappedKey = nacl2__default.default.secretbox(contentKey, kyberNonce, kyberKek);
593
- const keyMaterial = {
594
- x25519EphemeralPk: x25519EphKp.publicKey,
595
- x25519Nonce,
596
- x25519WrappedKey,
597
- kyberCiphertext: kyberResult.ciphertext,
598
- kyberNonce,
599
- kyberWrappedKey
600
- };
601
- const keyMaterialBytes = serializeHybridKeyMaterial(keyMaterial);
602
- const metadataBytes = serializeMetadata(metadata);
603
- const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
604
- const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
605
- const header = {
606
- version: OQE_FORMAT_VERSION,
607
- algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM,
608
- flags: 0,
609
- metadataLength: encryptedMetadata.length,
610
- keyMaterialLength: keyMaterialBytes.length,
611
- iv
612
- };
613
- const fileData = assembleOQEFile({
614
- header,
615
- keyMaterial: keyMaterialBytes,
616
- encryptedMetadata,
617
- encryptedContent
618
- });
619
- return {
620
- data: fileData,
621
- filename: addOQEExtension(metadata.filename),
622
- metadata,
623
- mode: "hybrid"
624
- };
662
+ try {
663
+ const metadataIv = rand12();
664
+ const contentIv = rand12();
665
+ const x25519EphKp = nacl3__default.default.box.keyPair();
666
+ const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
667
+ const x25519Shared = nacl3__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
668
+ const x25519EpkHex = toHex(x25519EphKp.publicKey);
669
+ const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
670
+ const kyberKemCtB64 = b64(kyberResult.ciphertext);
671
+ const kek = combinedFileKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
672
+ const ckWrapNonce = rand24();
673
+ const ckWrapped = nacl3__default.default.secretbox(contentKey, ckWrapNonce, kek);
674
+ kek.fill(0);
675
+ x25519Shared.fill(0);
676
+ kyberResult.sharedSecret.fill(0);
677
+ x25519EphKp.secretKey.fill(0);
678
+ const keyMaterial = {
679
+ x25519EphemeralPk: x25519EphKp.publicKey,
680
+ kyberCiphertext: kyberResult.ciphertext,
681
+ ckWrapNonce,
682
+ ckWrapped
683
+ };
684
+ const keyMaterialBytes = serializeHybridKeyMaterialV2(keyMaterial);
685
+ const metadataBytes = serializeMetadata(metadata);
686
+ const header = {
687
+ version: OQE_FORMAT_VERSION_V2,
688
+ algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM,
689
+ flags: 0,
690
+ metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
691
+ keyMaterialLength: keyMaterialBytes.length,
692
+ iv: metadataIv,
693
+ contentIv
694
+ };
695
+ const aad = writeOQEHeader(header);
696
+ const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
697
+ const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
698
+ const fileData = assembleOQEFile({
699
+ header,
700
+ keyMaterial: keyMaterialBytes,
701
+ encryptedMetadata,
702
+ encryptedContent
703
+ });
704
+ return {
705
+ data: fileData,
706
+ filename: addOQEExtension(metadata.filename),
707
+ metadata,
708
+ mode: "hybrid"
709
+ };
710
+ } finally {
711
+ contentKey.fill(0);
712
+ }
625
713
  }
626
714
  async function encryptPassword(plaintext, metadata, options) {
627
715
  if (!await isArgon2Available()) {
@@ -633,37 +721,44 @@ async function encryptPassword(plaintext, metadata, options) {
633
721
  };
634
722
  const salt = generateArgon2Salt(params.saltLength);
635
723
  const contentKey = await deriveKeyFromPassword(options.password, salt, params);
636
- const iv = rand12();
637
- const keyMaterial = {
638
- salt,
639
- memoryCost: params.memoryCost,
640
- timeCost: params.timeCost,
641
- parallelism: params.parallelism
642
- };
643
- const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
644
- const metadataBytes = serializeMetadata(metadata);
645
- const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
646
- const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
647
- const header = {
648
- version: OQE_FORMAT_VERSION,
649
- algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
650
- flags: 0,
651
- metadataLength: encryptedMetadata.length,
652
- keyMaterialLength: keyMaterialBytes.length,
653
- iv
654
- };
655
- const fileData = assembleOQEFile({
656
- header,
657
- keyMaterial: keyMaterialBytes,
658
- encryptedMetadata,
659
- encryptedContent
660
- });
661
- return {
662
- data: fileData,
663
- filename: addOQEExtension(metadata.filename),
664
- metadata,
665
- mode: "password"
666
- };
724
+ try {
725
+ const metadataIv = rand12();
726
+ const contentIv = rand12();
727
+ const keyMaterial = {
728
+ salt,
729
+ memoryCost: params.memoryCost,
730
+ timeCost: params.timeCost,
731
+ parallelism: params.parallelism
732
+ };
733
+ const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
734
+ const metadataBytes = serializeMetadata(metadata);
735
+ const header = {
736
+ version: OQE_FORMAT_VERSION_V2,
737
+ algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
738
+ flags: 0,
739
+ metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
740
+ keyMaterialLength: keyMaterialBytes.length,
741
+ iv: metadataIv,
742
+ contentIv
743
+ };
744
+ const aad = writeOQEHeader(header);
745
+ const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
746
+ const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
747
+ const fileData = assembleOQEFile({
748
+ header,
749
+ keyMaterial: keyMaterialBytes,
750
+ encryptedMetadata,
751
+ encryptedContent
752
+ });
753
+ return {
754
+ data: fileData,
755
+ filename: addOQEExtension(metadata.filename),
756
+ metadata,
757
+ mode: "password"
758
+ };
759
+ } finally {
760
+ contentKey.fill(0);
761
+ }
667
762
  }
668
763
  async function encryptFile(input, options) {
669
764
  const plaintext = await toUint8Array(input.data);
@@ -704,17 +799,56 @@ async function encryptFileWithPassword(input, password) {
704
799
  password
705
800
  });
706
801
  }
707
- function hkdfFlex2(ikm, salt, info) {
802
+ function hkdfFlex(ikm, salt, info) {
708
803
  return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
709
804
  }
805
+ function headerAad(header) {
806
+ return writeOQEHeader(header);
807
+ }
710
808
  async function decryptHybrid(encryptedData, options) {
711
- const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
712
- if (header.algorithmSuite !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
713
- throw new OQEError(
714
- "UNSUPPORTED_ALGORITHM",
715
- "This file was not encrypted with hybrid mode. Use password decryption."
716
- );
809
+ const parsed = parseOQEFile(encryptedData);
810
+ const { header } = parsed;
811
+ if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
812
+ return decryptHybridV2(parsed, options);
813
+ }
814
+ if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
815
+ return decryptHybridV1Legacy(parsed, options);
717
816
  }
817
+ throw new OQEError(
818
+ "UNSUPPORTED_ALGORITHM",
819
+ "This file was not encrypted with hybrid mode. Use password decryption."
820
+ );
821
+ }
822
+ async function decryptHybridV2(parsed, options) {
823
+ const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
824
+ if (header.version !== OQE_FORMAT_VERSION_V2 || !header.contentIv) {
825
+ throw new OQEError("INVALID_HEADER", "v2 hybrid file missing content IV");
826
+ }
827
+ const km = parseHybridKeyMaterialV2(keyMaterial);
828
+ const x25519EpkHex = toHex(km.x25519EphemeralPk);
829
+ const kyberKemCtB64 = b64(km.kyberCiphertext);
830
+ const kyberShared = await kyberDecapsulate(kyberKemCtB64, options.recipientSecretKeys.kyberSecB64);
831
+ const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
832
+ const x25519Shared = nacl3__default.default.scalarMult(sk, km.x25519EphemeralPk);
833
+ const kek = combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64);
834
+ kyberShared.fill(0);
835
+ x25519Shared.fill(0);
836
+ const contentKey = nacl3__default.default.secretbox.open(km.ckWrapped, km.ckWrapNonce, kek);
837
+ kek.fill(0);
838
+ if (!contentKey) {
839
+ throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key \u2014 combined-KEK authentication failed");
840
+ }
841
+ const aad = headerAad(header);
842
+ try {
843
+ const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, aad, "metadata");
844
+ const plaintext = await aesDecrypt(encryptedContent, contentKey, header.contentIv, aad);
845
+ return buildResult(plaintext, metadata, "hybrid");
846
+ } finally {
847
+ contentKey.fill(0);
848
+ }
849
+ }
850
+ async function decryptHybridV1Legacy(parsed, options) {
851
+ const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
718
852
  const km = parseHybridKeyMaterial(keyMaterial);
719
853
  let contentKey = null;
720
854
  if (await isKyberAvailable()) {
@@ -723,54 +857,47 @@ async function decryptHybrid(encryptedData, options) {
723
857
  toB64(km.kyberCiphertext),
724
858
  options.recipientSecretKeys.kyberSecB64
725
859
  );
726
- const kyberKek = hkdfFlex2(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
727
- const unwrapped = nacl2__default.default.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
728
- if (unwrapped) {
729
- contentKey = unwrapped;
730
- console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
731
- }
732
- } catch (e) {
733
- console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
860
+ const kyberKek = hkdfFlex(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
861
+ contentKey = nacl3__default.default.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
862
+ } catch {
734
863
  }
735
864
  }
736
865
  if (!contentKey) {
737
866
  try {
738
- const ephPk = km.x25519EphemeralPk;
739
867
  const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
740
- const x25519Shared = nacl2__default.default.scalarMult(sk, ephPk);
741
- const x25519Kek = hkdfFlex2(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
742
- const unwrapped = nacl2__default.default.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
743
- if (unwrapped) {
744
- contentKey = unwrapped;
745
- console.log("[OQE] Decrypted content key via X25519 (classical)");
746
- }
747
- } catch (e) {
748
- console.warn("[OQE] X25519 decryption failed:", e);
868
+ const x25519Shared = nacl3__default.default.scalarMult(sk, km.x25519EphemeralPk);
869
+ const x25519Kek = hkdfFlex(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
870
+ contentKey = nacl3__default.default.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
871
+ } catch {
749
872
  }
750
873
  }
751
874
  if (!contentKey) {
752
875
  throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
753
876
  }
754
- let metadata;
755
877
  try {
756
- const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
757
- metadata = parseMetadata(metadataBytes);
758
- } catch (e) {
759
- throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file metadata");
878
+ const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, void 0, "metadata");
879
+ const plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
880
+ return buildResult(plaintext, metadata, "hybrid");
881
+ } finally {
882
+ contentKey.fill(0);
760
883
  }
761
- let plaintext;
884
+ }
885
+ async function decryptSection(ciphertext, key, iv, aad, what) {
762
886
  try {
763
- plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
764
- } catch (e) {
765
- throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
887
+ const bytes = await aesDecrypt(ciphertext, key, iv, aad);
888
+ return parseMetadata(bytes);
889
+ } catch {
890
+ throw new OQEError("DECRYPTION_FAILED", `Failed to decrypt file ${what}`);
766
891
  }
892
+ }
893
+ function buildResult(plaintext, metadata, mode) {
767
894
  return {
768
895
  data: plaintext,
769
896
  filename: metadata.filename,
770
897
  mimeType: metadata.mimeType,
771
898
  originalSize: metadata.originalSize,
772
899
  metadata,
773
- mode: "hybrid"
900
+ mode
774
901
  };
775
902
  }
776
903
  async function decryptPassword(encryptedData, options) {
@@ -792,27 +919,27 @@ async function decryptPassword(encryptedData, options) {
792
919
  hashLength: 32,
793
920
  saltLength: km.salt.length
794
921
  });
795
- let metadata;
796
- try {
797
- const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
798
- metadata = parseMetadata(metadataBytes);
799
- } catch (e) {
800
- throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
801
- }
802
- let plaintext;
922
+ const isV2 = header.version === OQE_FORMAT_VERSION_V2;
923
+ const aad = isV2 ? headerAad(header) : void 0;
924
+ const contentIv = isV2 && header.contentIv ? header.contentIv : header.iv;
803
925
  try {
804
- plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
805
- } catch (e) {
806
- throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
926
+ let metadata;
927
+ try {
928
+ const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv, aad);
929
+ metadata = parseMetadata(metadataBytes);
930
+ } catch {
931
+ throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
932
+ }
933
+ let plaintext;
934
+ try {
935
+ plaintext = await aesDecrypt(encryptedContent, contentKey, contentIv, aad);
936
+ } catch {
937
+ throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
938
+ }
939
+ return buildResult(plaintext, metadata, "password");
940
+ } finally {
941
+ contentKey.fill(0);
807
942
  }
808
- return {
809
- data: plaintext,
810
- filename: metadata.filename,
811
- mimeType: metadata.mimeType,
812
- originalSize: metadata.originalSize,
813
- metadata,
814
- mode: "password"
815
- };
816
943
  }
817
944
  async function decryptFile(encryptedData, options) {
818
945
  const data = await toUint8Array(encryptedData);
@@ -842,9 +969,12 @@ async function inspectOQEFile(data) {
842
969
  const { header } = parseOQEFile(bytes);
843
970
  let mode;
844
971
  let algorithm;
845
- if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
972
+ if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
973
+ mode = "hybrid";
974
+ algorithm = "X25519 + ML-KEM-1024 + AES-256-GCM (AND-combined)";
975
+ } else if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
846
976
  mode = "hybrid";
847
- algorithm = "X25519 + Kyber768 + AES-256-GCM";
977
+ algorithm = "X25519 + Kyber + AES-256-GCM (legacy, either-key)";
848
978
  } else {
849
979
  mode = "password";
850
980
  algorithm = "Argon2id + AES-256-GCM";
@@ -1075,10 +1205,15 @@ exports.MIN_ARGON2ID_PARAMS = MIN_ARGON2ID_PARAMS;
1075
1205
  exports.OQEError = OQEError;
1076
1206
  exports.OQE_EXTENSION = OQE_EXTENSION;
1077
1207
  exports.OQE_FORMAT_VERSION = OQE_FORMAT_VERSION;
1208
+ exports.OQE_FORMAT_VERSION_V1 = OQE_FORMAT_VERSION_V1;
1209
+ exports.OQE_FORMAT_VERSION_V2 = OQE_FORMAT_VERSION_V2;
1078
1210
  exports.OQE_HEADER_SIZE = OQE_HEADER_SIZE;
1211
+ exports.OQE_HEADER_SIZE_V1 = OQE_HEADER_SIZE_V1;
1212
+ exports.OQE_HEADER_SIZE_V2 = OQE_HEADER_SIZE_V2;
1079
1213
  exports.OQE_MAGIC = OQE_MAGIC;
1080
1214
  exports.OQE_MIME_TYPE = OQE_MIME_TYPE;
1081
1215
  exports.STREAM_CHUNK_SIZE = STREAM_CHUNK_SIZE;
1216
+ exports.SUPPORTED_OQE_VERSIONS = SUPPORTED_OQE_VERSIONS;
1082
1217
  exports.addOQEExtension = addOQEExtension;
1083
1218
  exports.aesDecrypt = aesDecrypt;
1084
1219
  exports.aesDecryptCombined = aesDecryptCombined;
@@ -1089,6 +1224,7 @@ exports.aesEncryptStreaming = aesEncryptStreaming;
1089
1224
  exports.assembleOQEFile = assembleOQEFile;
1090
1225
  exports.benchmarkArgon2 = benchmarkArgon2;
1091
1226
  exports.bytesToDataURL = bytesToDataURL;
1227
+ exports.combinedFileKekV2 = combinedFileKekV2;
1092
1228
  exports.copyToClipboard = copyToClipboard;
1093
1229
  exports.createDropZone = createDropZone;
1094
1230
  exports.createObjectURL = createObjectURL;
@@ -1123,7 +1259,9 @@ exports.isWebCryptoAvailable = isWebCryptoAvailable;
1123
1259
  exports.openFilePicker = openFilePicker;
1124
1260
  exports.openFileToEncrypt = openFileToEncrypt;
1125
1261
  exports.openOQEFilePicker = openOQEFilePicker;
1262
+ exports.oqeHeaderSize = oqeHeaderSize;
1126
1263
  exports.parseHybridKeyMaterial = parseHybridKeyMaterial;
1264
+ exports.parseHybridKeyMaterialV2 = parseHybridKeyMaterialV2;
1127
1265
  exports.parseMetadata = parseMetadata;
1128
1266
  exports.parseOQEFile = parseOQEFile;
1129
1267
  exports.parseOQEHeader = parseOQEHeader;
@@ -1133,6 +1271,7 @@ exports.readFileAsDataURL = readFileAsDataURL;
1133
1271
  exports.readFileAsText = readFileAsText;
1134
1272
  exports.removeOQEExtension = removeOQEExtension;
1135
1273
  exports.serializeHybridKeyMaterial = serializeHybridKeyMaterial;
1274
+ exports.serializeHybridKeyMaterialV2 = serializeHybridKeyMaterialV2;
1136
1275
  exports.serializeMetadata = serializeMetadata;
1137
1276
  exports.serializePasswordKeyMaterial = serializePasswordKeyMaterial;
1138
1277
  exports.toUint8Array = toUint8Array;