@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.
- package/dist/crypto/index.cjs +81 -54
- package/dist/crypto/index.d.cts +33 -11
- package/dist/crypto/index.d.ts +33 -11
- package/dist/crypto/index.js +81 -54
- package/dist/{decrypt-eSHlbh1j.d.cts → decrypt-O1iqFIDL.d.cts} +80 -6
- package/dist/{decrypt-eSHlbh1j.d.ts → decrypt-O1iqFIDL.d.ts} +80 -6
- package/dist/fs/index.cjs +286 -147
- package/dist/fs/index.d.cts +28 -7
- package/dist/fs/index.d.ts +28 -7
- package/dist/fs/index.js +277 -147
- package/dist/index.cjs +358 -229
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +357 -230
- package/dist/{integrity-BenoFsmP.d.cts → integrity-BkBDrHA-.d.cts} +12 -4
- package/dist/{integrity-D9J98Bty.d.ts → integrity-DsFJv5c-.d.ts} +12 -4
- package/dist/{types-CRka8PC7.d.cts → types-DmnVueAY.d.cts} +14 -4
- package/dist/{types-CRka8PC7.d.ts → types-DmnVueAY.d.ts} +14 -4
- package/dist/utils/index.cjs +7 -10
- package/dist/utils/index.d.cts +2 -2
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +7 -10
- package/dist/vault/index.cjs +8 -11
- package/dist/vault/index.d.cts +2 -2
- package/dist/vault/index.d.ts +2 -2
- package/dist/vault/index.js +8 -11
- package/package.json +7 -7
- package/src/crypto/dilithium.ts +4 -4
- package/src/crypto/hybrid.ts +182 -80
- package/src/crypto/index.ts +2 -0
- package/src/fs/decrypt.ts +145 -68
- package/src/fs/encrypt.ts +161 -112
- package/src/fs/format.ts +110 -15
- package/src/fs/index.ts +4 -0
- package/src/fs/types.ts +77 -6
- package/src/index.ts +4 -0
- package/src/utils/integrity.ts +16 -15
- package/src/vault/manager.ts +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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 <
|
|
387
|
-
throw new OQEError("INVALID_HEADER", `File too small: need ${
|
|
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
|
|
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 =
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
encryptedMetadata,
|
|
617
|
-
encryptedContent
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
header
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
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
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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 =
|
|
727
|
-
|
|
728
|
-
|
|
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 =
|
|
741
|
-
const x25519Kek =
|
|
742
|
-
|
|
743
|
-
|
|
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
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
884
|
+
}
|
|
885
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
762
886
|
try {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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.
|
|
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 +
|
|
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;
|