@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.
- package/dist/crypto/index.cjs +91 -54
- package/dist/crypto/index.d.cts +43 -11
- package/dist/crypto/index.d.ts +43 -11
- package/dist/crypto/index.js +87 -55
- 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 +368 -229
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +362 -230
- package/dist/{integrity-ByMp24VA.d.cts → integrity-BkBDrHA-.d.cts} +12 -4
- package/dist/{integrity-BPvNsC50.d.ts → integrity-DsFJv5c-.d.ts} +12 -4
- package/dist/{types-Dx_AFqow.d.cts → types-DmnVueAY.d.cts} +15 -5
- package/dist/{types-Dx_AFqow.d.ts → types-DmnVueAY.d.ts} +15 -5
- 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 +19 -25
- package/src/crypto/dilithium.ts +4 -4
- package/src/crypto/hybrid.ts +182 -80
- package/src/crypto/index.ts +8 -1
- package/src/crypto/kyber.ts +20 -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 +9 -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.js
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import { argon2id } from 'hash-wasm';
|
|
2
2
|
import { sha256 as sha256$1 } from '@noble/hashes/sha2.js';
|
|
3
3
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
4
|
-
import
|
|
4
|
+
import nacl3 from 'tweetnacl';
|
|
5
5
|
import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
|
|
6
6
|
|
|
7
7
|
// src/fs/types.ts
|
|
8
8
|
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
9
|
-
var
|
|
9
|
+
var OQE_FORMAT_VERSION_V1 = 1;
|
|
10
|
+
var OQE_FORMAT_VERSION_V2 = 2;
|
|
11
|
+
var OQE_FORMAT_VERSION = OQE_FORMAT_VERSION_V2;
|
|
12
|
+
var SUPPORTED_OQE_VERSIONS = [OQE_FORMAT_VERSION_V1, OQE_FORMAT_VERSION_V2];
|
|
10
13
|
var ALGORITHM_SUITES = {
|
|
11
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
|
|
16
|
+
* wraps (either secret unwraps). Only appears in v1 files. Never written.
|
|
17
|
+
*/
|
|
12
18
|
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
13
19
|
/** Password: Argon2id + AES-256-GCM */
|
|
14
|
-
PASSWORD_ARGON2ID_AES256GCM: 2
|
|
20
|
+
PASSWORD_ARGON2ID_AES256GCM: 2,
|
|
21
|
+
/**
|
|
22
|
+
* Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
|
|
23
|
+
* (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
|
|
24
|
+
*/
|
|
25
|
+
HYBRID_X25519_MLKEM1024_AES256GCM: 3
|
|
15
26
|
};
|
|
16
27
|
var DEFAULT_ARGON2ID_PARAMS = {
|
|
17
28
|
memoryCost: 65536,
|
|
@@ -29,7 +40,9 @@ var MIN_ARGON2ID_PARAMS = {
|
|
|
29
40
|
hashLength: 32,
|
|
30
41
|
saltLength: 32
|
|
31
42
|
};
|
|
32
|
-
var
|
|
43
|
+
var OQE_HEADER_SIZE_V1 = 30;
|
|
44
|
+
var OQE_HEADER_SIZE_V2 = 42;
|
|
45
|
+
var OQE_HEADER_SIZE = OQE_HEADER_SIZE_V1;
|
|
33
46
|
var OQEError = class extends Error {
|
|
34
47
|
constructor(code, message) {
|
|
35
48
|
super(message);
|
|
@@ -115,6 +128,7 @@ function hkdfSha256(ikm, opts) {
|
|
|
115
128
|
}
|
|
116
129
|
return out;
|
|
117
130
|
}
|
|
131
|
+
var b64 = toB64;
|
|
118
132
|
var ub64 = fromB64;
|
|
119
133
|
var u8 = (s) => typeof s === "string" ? textEncoder.encode(s) : s;
|
|
120
134
|
|
|
@@ -344,6 +358,9 @@ async function aesDecryptStreaming(encrypted, key, onProgress) {
|
|
|
344
358
|
}
|
|
345
359
|
|
|
346
360
|
// src/fs/format.ts
|
|
361
|
+
function oqeHeaderSize(version) {
|
|
362
|
+
return version === OQE_FORMAT_VERSION_V2 ? OQE_HEADER_SIZE_V2 : OQE_HEADER_SIZE_V1;
|
|
363
|
+
}
|
|
347
364
|
function writeUint32BE(value) {
|
|
348
365
|
const buffer = new ArrayBuffer(4);
|
|
349
366
|
new DataView(buffer).setUint32(0, value, false);
|
|
@@ -361,7 +378,8 @@ function readUint16BE(data, offset) {
|
|
|
361
378
|
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
362
379
|
}
|
|
363
380
|
function writeOQEHeader(header) {
|
|
364
|
-
const
|
|
381
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
382
|
+
const buffer = new Uint8Array(oqeHeaderSize(header.version));
|
|
365
383
|
let offset = 0;
|
|
366
384
|
buffer.set(OQE_MAGIC, offset);
|
|
367
385
|
offset += 4;
|
|
@@ -374,11 +392,18 @@ function writeOQEHeader(header) {
|
|
|
374
392
|
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
375
393
|
offset += 4;
|
|
376
394
|
buffer.set(header.iv, offset);
|
|
395
|
+
offset += 12;
|
|
396
|
+
if (isV2) {
|
|
397
|
+
if (!header.contentIv || header.contentIv.length !== 12) {
|
|
398
|
+
throw new OQEError("INVALID_HEADER", "v2 header requires a 12-byte contentIv");
|
|
399
|
+
}
|
|
400
|
+
buffer.set(header.contentIv, offset);
|
|
401
|
+
}
|
|
377
402
|
return buffer;
|
|
378
403
|
}
|
|
379
404
|
function parseOQEHeader(data) {
|
|
380
|
-
if (data.length <
|
|
381
|
-
throw new OQEError("INVALID_HEADER", `File too small: need ${
|
|
405
|
+
if (data.length < OQE_HEADER_SIZE_V1) {
|
|
406
|
+
throw new OQEError("INVALID_HEADER", `File too small: need at least ${OQE_HEADER_SIZE_V1} bytes, got ${data.length}`);
|
|
382
407
|
}
|
|
383
408
|
let offset = 0;
|
|
384
409
|
const magic = data.slice(0, 4);
|
|
@@ -387,14 +412,19 @@ function parseOQEHeader(data) {
|
|
|
387
412
|
}
|
|
388
413
|
offset += 4;
|
|
389
414
|
const version = data[offset++];
|
|
390
|
-
if (version
|
|
415
|
+
if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
|
|
391
416
|
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
392
417
|
}
|
|
393
418
|
const algorithmSuiteRaw = data[offset++];
|
|
394
|
-
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
419
|
+
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
395
420
|
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
396
421
|
}
|
|
397
422
|
const algorithmSuite = algorithmSuiteRaw;
|
|
423
|
+
const isV2 = version === OQE_FORMAT_VERSION_V2;
|
|
424
|
+
const headerSize = oqeHeaderSize(version);
|
|
425
|
+
if (data.length < headerSize) {
|
|
426
|
+
throw new OQEError("INVALID_HEADER", `Truncated v${version} header: need ${headerSize} bytes, got ${data.length}`);
|
|
427
|
+
}
|
|
398
428
|
const flags = readUint32BE(data, offset);
|
|
399
429
|
offset += 4;
|
|
400
430
|
const metadataLength = readUint32BE(data, offset);
|
|
@@ -402,13 +432,16 @@ function parseOQEHeader(data) {
|
|
|
402
432
|
const keyMaterialLength = readUint32BE(data, offset);
|
|
403
433
|
offset += 4;
|
|
404
434
|
const iv = data.slice(offset, offset + 12);
|
|
435
|
+
offset += 12;
|
|
436
|
+
const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
|
|
405
437
|
return {
|
|
406
438
|
version,
|
|
407
439
|
algorithmSuite,
|
|
408
440
|
flags,
|
|
409
441
|
metadataLength,
|
|
410
442
|
keyMaterialLength,
|
|
411
|
-
iv
|
|
443
|
+
iv,
|
|
444
|
+
contentIv
|
|
412
445
|
};
|
|
413
446
|
}
|
|
414
447
|
function serializeHybridKeyMaterial(km) {
|
|
@@ -459,6 +492,38 @@ function parseHybridKeyMaterial(data) {
|
|
|
459
492
|
kyberWrappedKey
|
|
460
493
|
};
|
|
461
494
|
}
|
|
495
|
+
function serializeHybridKeyMaterialV2(km) {
|
|
496
|
+
const parts = [];
|
|
497
|
+
parts.push(km.x25519EphemeralPk);
|
|
498
|
+
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
499
|
+
parts.push(km.kyberCiphertext);
|
|
500
|
+
parts.push(km.ckWrapNonce);
|
|
501
|
+
parts.push(writeUint16BE(km.ckWrapped.length));
|
|
502
|
+
parts.push(km.ckWrapped);
|
|
503
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
504
|
+
const result = new Uint8Array(totalLength);
|
|
505
|
+
let offset = 0;
|
|
506
|
+
for (const part of parts) {
|
|
507
|
+
result.set(part, offset);
|
|
508
|
+
offset += part.length;
|
|
509
|
+
}
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
function parseHybridKeyMaterialV2(data) {
|
|
513
|
+
let offset = 0;
|
|
514
|
+
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
515
|
+
offset += 32;
|
|
516
|
+
const kyberCtLen = readUint16BE(data, offset);
|
|
517
|
+
offset += 2;
|
|
518
|
+
const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
|
|
519
|
+
offset += kyberCtLen;
|
|
520
|
+
const ckWrapNonce = data.slice(offset, offset + 24);
|
|
521
|
+
offset += 24;
|
|
522
|
+
const ckWrappedLen = readUint16BE(data, offset);
|
|
523
|
+
offset += 2;
|
|
524
|
+
const ckWrapped = data.slice(offset, offset + ckWrappedLen);
|
|
525
|
+
return { x25519EphemeralPk, kyberCiphertext, ckWrapNonce, ckWrapped };
|
|
526
|
+
}
|
|
462
527
|
function serializePasswordKeyMaterial(km) {
|
|
463
528
|
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
464
529
|
result.set(km.salt, 0);
|
|
@@ -500,7 +565,7 @@ function assembleOQEFile(components) {
|
|
|
500
565
|
}
|
|
501
566
|
function parseOQEFile(data) {
|
|
502
567
|
const header = parseOQEHeader(data);
|
|
503
|
-
let offset =
|
|
568
|
+
let offset = oqeHeaderSize(header.version);
|
|
504
569
|
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
505
570
|
offset += header.keyMaterialLength;
|
|
506
571
|
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
@@ -535,8 +600,11 @@ function isOQEFile(filenameOrData) {
|
|
|
535
600
|
}
|
|
536
601
|
var OQE_MIME_TYPE = "application/x-omnituum-encrypted";
|
|
537
602
|
function getAlgorithmName(suiteId) {
|
|
603
|
+
if (suiteId === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
|
|
604
|
+
return "Hybrid (X25519 + ML-KEM-1024 + AES-256-GCM)";
|
|
605
|
+
}
|
|
538
606
|
if (suiteId === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
539
|
-
return "Hybrid (X25519 +
|
|
607
|
+
return "Hybrid legacy (X25519 + Kyber + AES-256-GCM, either-key)";
|
|
540
608
|
}
|
|
541
609
|
if (suiteId === ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
542
610
|
return "Password (Argon2id + AES-256-GCM)";
|
|
@@ -561,61 +629,81 @@ async function kyberDecapsulate(kemCiphertextB64, secretKeyB64) {
|
|
|
561
629
|
}
|
|
562
630
|
|
|
563
631
|
// src/fs/encrypt.ts
|
|
564
|
-
|
|
565
|
-
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
566
|
-
}
|
|
632
|
+
var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
|
|
567
633
|
function computeIdentityHash(publicKeyHex) {
|
|
568
634
|
const hash = sha256(fromHex(publicKeyHex));
|
|
569
635
|
return toHex(hash).slice(0, 16);
|
|
570
636
|
}
|
|
637
|
+
function combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
638
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
639
|
+
ikm.set(kyberShared, 0);
|
|
640
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
641
|
+
try {
|
|
642
|
+
return hkdfSha256(ikm, {
|
|
643
|
+
salt: u8("omnituum/fs/hybrid-v2"),
|
|
644
|
+
info: u8(`wrap-content-key|${x25519EpkHex}|${kyberKemCtB64}`),
|
|
645
|
+
length: 32
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
ikm.fill(0);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
571
651
|
async function encryptHybrid(plaintext, metadata, options) {
|
|
572
652
|
if (!await isKyberAvailable()) {
|
|
573
653
|
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
574
654
|
}
|
|
575
655
|
const contentKey = rand32();
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
encryptedMetadata,
|
|
611
|
-
encryptedContent
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
656
|
+
try {
|
|
657
|
+
const metadataIv = rand12();
|
|
658
|
+
const contentIv = rand12();
|
|
659
|
+
const x25519EphKp = nacl3.box.keyPair();
|
|
660
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
661
|
+
const x25519Shared = nacl3.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
662
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
663
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
664
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
665
|
+
const kek = combinedFileKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
666
|
+
const ckWrapNonce = rand24();
|
|
667
|
+
const ckWrapped = nacl3.secretbox(contentKey, ckWrapNonce, kek);
|
|
668
|
+
kek.fill(0);
|
|
669
|
+
x25519Shared.fill(0);
|
|
670
|
+
kyberResult.sharedSecret.fill(0);
|
|
671
|
+
x25519EphKp.secretKey.fill(0);
|
|
672
|
+
const keyMaterial = {
|
|
673
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
674
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
675
|
+
ckWrapNonce,
|
|
676
|
+
ckWrapped
|
|
677
|
+
};
|
|
678
|
+
const keyMaterialBytes = serializeHybridKeyMaterialV2(keyMaterial);
|
|
679
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
680
|
+
const header = {
|
|
681
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
682
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM,
|
|
683
|
+
flags: 0,
|
|
684
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
685
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
686
|
+
iv: metadataIv,
|
|
687
|
+
contentIv
|
|
688
|
+
};
|
|
689
|
+
const aad = writeOQEHeader(header);
|
|
690
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
691
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
692
|
+
const fileData = assembleOQEFile({
|
|
693
|
+
header,
|
|
694
|
+
keyMaterial: keyMaterialBytes,
|
|
695
|
+
encryptedMetadata,
|
|
696
|
+
encryptedContent
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
data: fileData,
|
|
700
|
+
filename: addOQEExtension(metadata.filename),
|
|
701
|
+
metadata,
|
|
702
|
+
mode: "hybrid"
|
|
703
|
+
};
|
|
704
|
+
} finally {
|
|
705
|
+
contentKey.fill(0);
|
|
706
|
+
}
|
|
619
707
|
}
|
|
620
708
|
async function encryptPassword(plaintext, metadata, options) {
|
|
621
709
|
if (!await isArgon2Available()) {
|
|
@@ -627,37 +715,44 @@ async function encryptPassword(plaintext, metadata, options) {
|
|
|
627
715
|
};
|
|
628
716
|
const salt = generateArgon2Salt(params.saltLength);
|
|
629
717
|
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
header
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
718
|
+
try {
|
|
719
|
+
const metadataIv = rand12();
|
|
720
|
+
const contentIv = rand12();
|
|
721
|
+
const keyMaterial = {
|
|
722
|
+
salt,
|
|
723
|
+
memoryCost: params.memoryCost,
|
|
724
|
+
timeCost: params.timeCost,
|
|
725
|
+
parallelism: params.parallelism
|
|
726
|
+
};
|
|
727
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
728
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
729
|
+
const header = {
|
|
730
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
731
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
732
|
+
flags: 0,
|
|
733
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
734
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
735
|
+
iv: metadataIv,
|
|
736
|
+
contentIv
|
|
737
|
+
};
|
|
738
|
+
const aad = writeOQEHeader(header);
|
|
739
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
740
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
741
|
+
const fileData = assembleOQEFile({
|
|
742
|
+
header,
|
|
743
|
+
keyMaterial: keyMaterialBytes,
|
|
744
|
+
encryptedMetadata,
|
|
745
|
+
encryptedContent
|
|
746
|
+
});
|
|
747
|
+
return {
|
|
748
|
+
data: fileData,
|
|
749
|
+
filename: addOQEExtension(metadata.filename),
|
|
750
|
+
metadata,
|
|
751
|
+
mode: "password"
|
|
752
|
+
};
|
|
753
|
+
} finally {
|
|
754
|
+
contentKey.fill(0);
|
|
755
|
+
}
|
|
661
756
|
}
|
|
662
757
|
async function encryptFile(input, options) {
|
|
663
758
|
const plaintext = await toUint8Array(input.data);
|
|
@@ -698,17 +793,56 @@ async function encryptFileWithPassword(input, password) {
|
|
|
698
793
|
password
|
|
699
794
|
});
|
|
700
795
|
}
|
|
701
|
-
function
|
|
796
|
+
function hkdfFlex(ikm, salt, info) {
|
|
702
797
|
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
703
798
|
}
|
|
799
|
+
function headerAad(header) {
|
|
800
|
+
return writeOQEHeader(header);
|
|
801
|
+
}
|
|
704
802
|
async function decryptHybrid(encryptedData, options) {
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
803
|
+
const parsed = parseOQEFile(encryptedData);
|
|
804
|
+
const { header } = parsed;
|
|
805
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
|
|
806
|
+
return decryptHybridV2(parsed, options);
|
|
807
|
+
}
|
|
808
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
809
|
+
return decryptHybridV1Legacy(parsed, options);
|
|
711
810
|
}
|
|
811
|
+
throw new OQEError(
|
|
812
|
+
"UNSUPPORTED_ALGORITHM",
|
|
813
|
+
"This file was not encrypted with hybrid mode. Use password decryption."
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
async function decryptHybridV2(parsed, options) {
|
|
817
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
818
|
+
if (header.version !== OQE_FORMAT_VERSION_V2 || !header.contentIv) {
|
|
819
|
+
throw new OQEError("INVALID_HEADER", "v2 hybrid file missing content IV");
|
|
820
|
+
}
|
|
821
|
+
const km = parseHybridKeyMaterialV2(keyMaterial);
|
|
822
|
+
const x25519EpkHex = toHex(km.x25519EphemeralPk);
|
|
823
|
+
const kyberKemCtB64 = b64(km.kyberCiphertext);
|
|
824
|
+
const kyberShared = await kyberDecapsulate(kyberKemCtB64, options.recipientSecretKeys.kyberSecB64);
|
|
825
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
826
|
+
const x25519Shared = nacl3.scalarMult(sk, km.x25519EphemeralPk);
|
|
827
|
+
const kek = combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
828
|
+
kyberShared.fill(0);
|
|
829
|
+
x25519Shared.fill(0);
|
|
830
|
+
const contentKey = nacl3.secretbox.open(km.ckWrapped, km.ckWrapNonce, kek);
|
|
831
|
+
kek.fill(0);
|
|
832
|
+
if (!contentKey) {
|
|
833
|
+
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
834
|
+
}
|
|
835
|
+
const aad = headerAad(header);
|
|
836
|
+
try {
|
|
837
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, aad, "metadata");
|
|
838
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.contentIv, aad);
|
|
839
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
840
|
+
} finally {
|
|
841
|
+
contentKey.fill(0);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async function decryptHybridV1Legacy(parsed, options) {
|
|
845
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
712
846
|
const km = parseHybridKeyMaterial(keyMaterial);
|
|
713
847
|
let contentKey = null;
|
|
714
848
|
if (await isKyberAvailable()) {
|
|
@@ -717,54 +851,47 @@ async function decryptHybrid(encryptedData, options) {
|
|
|
717
851
|
toB64(km.kyberCiphertext),
|
|
718
852
|
options.recipientSecretKeys.kyberSecB64
|
|
719
853
|
);
|
|
720
|
-
const kyberKek =
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
contentKey = unwrapped;
|
|
724
|
-
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
725
|
-
}
|
|
726
|
-
} catch (e) {
|
|
727
|
-
console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
|
|
854
|
+
const kyberKek = hkdfFlex(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
|
|
855
|
+
contentKey = nacl3.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
856
|
+
} catch {
|
|
728
857
|
}
|
|
729
858
|
}
|
|
730
859
|
if (!contentKey) {
|
|
731
860
|
try {
|
|
732
|
-
const ephPk = km.x25519EphemeralPk;
|
|
733
861
|
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
734
|
-
const x25519Shared =
|
|
735
|
-
const x25519Kek =
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
contentKey = unwrapped;
|
|
739
|
-
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
740
|
-
}
|
|
741
|
-
} catch (e) {
|
|
742
|
-
console.warn("[OQE] X25519 decryption failed:", e);
|
|
862
|
+
const x25519Shared = nacl3.scalarMult(sk, km.x25519EphemeralPk);
|
|
863
|
+
const x25519Kek = hkdfFlex(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
864
|
+
contentKey = nacl3.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
865
|
+
} catch {
|
|
743
866
|
}
|
|
744
867
|
}
|
|
745
868
|
if (!contentKey) {
|
|
746
869
|
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
747
870
|
}
|
|
748
|
-
let metadata;
|
|
749
871
|
try {
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
872
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, void 0, "metadata");
|
|
873
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
874
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
875
|
+
} finally {
|
|
876
|
+
contentKey.fill(0);
|
|
754
877
|
}
|
|
755
|
-
|
|
878
|
+
}
|
|
879
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
756
880
|
try {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
881
|
+
const bytes = await aesDecrypt(ciphertext, key, iv, aad);
|
|
882
|
+
return parseMetadata(bytes);
|
|
883
|
+
} catch {
|
|
884
|
+
throw new OQEError("DECRYPTION_FAILED", `Failed to decrypt file ${what}`);
|
|
760
885
|
}
|
|
886
|
+
}
|
|
887
|
+
function buildResult(plaintext, metadata, mode) {
|
|
761
888
|
return {
|
|
762
889
|
data: plaintext,
|
|
763
890
|
filename: metadata.filename,
|
|
764
891
|
mimeType: metadata.mimeType,
|
|
765
892
|
originalSize: metadata.originalSize,
|
|
766
893
|
metadata,
|
|
767
|
-
mode
|
|
894
|
+
mode
|
|
768
895
|
};
|
|
769
896
|
}
|
|
770
897
|
async function decryptPassword(encryptedData, options) {
|
|
@@ -786,27 +913,27 @@ async function decryptPassword(encryptedData, options) {
|
|
|
786
913
|
hashLength: 32,
|
|
787
914
|
saltLength: km.salt.length
|
|
788
915
|
});
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
metadata = parseMetadata(metadataBytes);
|
|
793
|
-
} catch (e) {
|
|
794
|
-
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
795
|
-
}
|
|
796
|
-
let plaintext;
|
|
916
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
917
|
+
const aad = isV2 ? headerAad(header) : void 0;
|
|
918
|
+
const contentIv = isV2 && header.contentIv ? header.contentIv : header.iv;
|
|
797
919
|
try {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
920
|
+
let metadata;
|
|
921
|
+
try {
|
|
922
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv, aad);
|
|
923
|
+
metadata = parseMetadata(metadataBytes);
|
|
924
|
+
} catch {
|
|
925
|
+
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
926
|
+
}
|
|
927
|
+
let plaintext;
|
|
928
|
+
try {
|
|
929
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, contentIv, aad);
|
|
930
|
+
} catch {
|
|
931
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
932
|
+
}
|
|
933
|
+
return buildResult(plaintext, metadata, "password");
|
|
934
|
+
} finally {
|
|
935
|
+
contentKey.fill(0);
|
|
801
936
|
}
|
|
802
|
-
return {
|
|
803
|
-
data: plaintext,
|
|
804
|
-
filename: metadata.filename,
|
|
805
|
-
mimeType: metadata.mimeType,
|
|
806
|
-
originalSize: metadata.originalSize,
|
|
807
|
-
metadata,
|
|
808
|
-
mode: "password"
|
|
809
|
-
};
|
|
810
937
|
}
|
|
811
938
|
async function decryptFile(encryptedData, options) {
|
|
812
939
|
const data = await toUint8Array(encryptedData);
|
|
@@ -836,9 +963,12 @@ async function inspectOQEFile(data) {
|
|
|
836
963
|
const { header } = parseOQEFile(bytes);
|
|
837
964
|
let mode;
|
|
838
965
|
let algorithm;
|
|
839
|
-
if (header.algorithmSuite === ALGORITHM_SUITES.
|
|
966
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
|
|
967
|
+
mode = "hybrid";
|
|
968
|
+
algorithm = "X25519 + ML-KEM-1024 + AES-256-GCM (AND-combined)";
|
|
969
|
+
} else if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
840
970
|
mode = "hybrid";
|
|
841
|
-
algorithm = "X25519 +
|
|
971
|
+
algorithm = "X25519 + Kyber + AES-256-GCM (legacy, either-key)";
|
|
842
972
|
} else {
|
|
843
973
|
mode = "password";
|
|
844
974
|
algorithm = "Argon2id + AES-256-GCM";
|
|
@@ -1060,4 +1190,4 @@ function isDragDropSupported() {
|
|
|
1060
1190
|
return "draggable" in div || "ondragstart" in div && "ondrop" in div;
|
|
1061
1191
|
}
|
|
1062
1192
|
|
|
1063
|
-
export { AES_GCM_IV_SIZE, AES_GCM_TAG_SIZE, AES_KEY_SIZE, ALGORITHM_SUITES, DEFAULT_ARGON2ID_PARAMS, MIN_ARGON2ID_PARAMS, OQEError, OQE_EXTENSION, OQE_FORMAT_VERSION, OQE_HEADER_SIZE, OQE_MAGIC, OQE_MIME_TYPE, STREAM_CHUNK_SIZE, addOQEExtension, aesDecrypt, aesDecryptCombined, aesDecryptStreaming, aesEncrypt, aesEncryptCombined, aesEncryptStreaming, assembleOQEFile, benchmarkArgon2, bytesToDataURL, copyToClipboard, createDropZone, createObjectURL, decryptFile, decryptFileForSelf, decryptFileWithPassword, decryptResultToBlob, deriveKeyFromPassword, downloadBlob, downloadBytes, downloadDecryptedFile, downloadEncryptedFile, encryptFile, encryptFileForSelf, encryptFileWithPassword, encryptResultToBlob, estimateArgon2Params, exportAesKey, formatFileSize, generateAesKey, generateArgon2Salt, getAlgorithmName, getFileInfo, importAesKey, inspectOQEFile, isArgon2Available, isBrowser, isDragDropSupported, isFileAPIAvailable, isOQEFile, isWebCryptoAvailable, openFilePicker, openFileToEncrypt, openOQEFilePicker, parseHybridKeyMaterial, parseMetadata, parseOQEFile, parseOQEHeader, parsePasswordKeyMaterial, readFile, readFileAsDataURL, readFileAsText, removeOQEExtension, serializeHybridKeyMaterial, serializeMetadata, serializePasswordKeyMaterial, toUint8Array, verifyPassword, writeOQEHeader };
|
|
1193
|
+
export { AES_GCM_IV_SIZE, AES_GCM_TAG_SIZE, AES_KEY_SIZE, ALGORITHM_SUITES, DEFAULT_ARGON2ID_PARAMS, MIN_ARGON2ID_PARAMS, OQEError, OQE_EXTENSION, OQE_FORMAT_VERSION, OQE_FORMAT_VERSION_V1, OQE_FORMAT_VERSION_V2, OQE_HEADER_SIZE, OQE_HEADER_SIZE_V1, OQE_HEADER_SIZE_V2, OQE_MAGIC, OQE_MIME_TYPE, STREAM_CHUNK_SIZE, SUPPORTED_OQE_VERSIONS, addOQEExtension, aesDecrypt, aesDecryptCombined, aesDecryptStreaming, aesEncrypt, aesEncryptCombined, aesEncryptStreaming, assembleOQEFile, benchmarkArgon2, bytesToDataURL, combinedFileKekV2, copyToClipboard, createDropZone, createObjectURL, decryptFile, decryptFileForSelf, decryptFileWithPassword, decryptResultToBlob, deriveKeyFromPassword, downloadBlob, downloadBytes, downloadDecryptedFile, downloadEncryptedFile, encryptFile, encryptFileForSelf, encryptFileWithPassword, encryptResultToBlob, estimateArgon2Params, exportAesKey, formatFileSize, generateAesKey, generateArgon2Salt, getAlgorithmName, getFileInfo, importAesKey, inspectOQEFile, isArgon2Available, isBrowser, isDragDropSupported, isFileAPIAvailable, isOQEFile, isWebCryptoAvailable, openFilePicker, openFileToEncrypt, openOQEFilePicker, oqeHeaderSize, parseHybridKeyMaterial, parseHybridKeyMaterialV2, parseMetadata, parseOQEFile, parseOQEHeader, parsePasswordKeyMaterial, readFile, readFileAsDataURL, readFileAsText, removeOQEExtension, serializeHybridKeyMaterial, serializeHybridKeyMaterialV2, serializeMetadata, serializePasswordKeyMaterial, toUint8Array, verifyPassword, writeOQEHeader };
|