@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/index.cjs
CHANGED
|
@@ -207,17 +207,20 @@ function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
|
207
207
|
return nacl4__default.default.secretbox.open(wrapped, nonce, kek) || null;
|
|
208
208
|
}
|
|
209
209
|
var ENVELOPE_VERSION = envelopeRegistry.OMNI_VERSIONS.HYBRID_V1;
|
|
210
|
+
var ENVELOPE_VERSION_V2 = envelopeRegistry.OMNI_VERSIONS.HYBRID_V2;
|
|
210
211
|
var ENVELOPE_VERSION_LEGACY = envelopeRegistry.DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
|
|
211
212
|
var VAULT_VERSION = "omnituum.vault.v1";
|
|
212
213
|
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
213
214
|
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
214
215
|
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
216
|
+
var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
|
|
215
217
|
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
216
218
|
var VAULT_KDF = "PBKDF2-SHA256";
|
|
217
219
|
var VAULT_KDF_V2 = "Argon2id";
|
|
218
220
|
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
219
221
|
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
220
222
|
ENVELOPE_VERSION,
|
|
223
|
+
ENVELOPE_VERSION_V2,
|
|
221
224
|
ENVELOPE_VERSION_LEGACY
|
|
222
225
|
];
|
|
223
226
|
var SUPPORTED_VAULT_VERSIONS = [
|
|
@@ -262,6 +265,8 @@ function isVaultVersionSupported(version) {
|
|
|
262
265
|
function isVaultEncryptedVersionSupported(version) {
|
|
263
266
|
return SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version);
|
|
264
267
|
}
|
|
268
|
+
var ENVELOPE_V1_FIELDS = ["x25519Epk", "x25519Wrap", "kyberKemCt", "kyberWrap", "contentNonce", "ciphertext", "meta"];
|
|
269
|
+
var ENVELOPE_V2_FIELDS = ["x25519Epk", "kyberKemCt", "ckWrap", "contentNonce", "ciphertext", "meta"];
|
|
265
270
|
function validateEnvelope(envelope) {
|
|
266
271
|
const errors = [];
|
|
267
272
|
if (!envelope || typeof envelope !== "object") {
|
|
@@ -273,14 +278,16 @@ function validateEnvelope(envelope) {
|
|
|
273
278
|
} else if (!isEnvelopeVersionSupported(env.v)) {
|
|
274
279
|
errors.push(`Unsupported envelope version: ${env.v}`);
|
|
275
280
|
}
|
|
276
|
-
|
|
277
|
-
|
|
281
|
+
const isV2 = env.v === ENVELOPE_VERSION_V2;
|
|
282
|
+
const expectedSuite = isV2 ? ENVELOPE_SUITE_V2 : ENVELOPE_SUITE;
|
|
283
|
+
const requiredFields = isV2 ? ENVELOPE_V2_FIELDS : ENVELOPE_V1_FIELDS;
|
|
284
|
+
if (env.suite !== expectedSuite) {
|
|
285
|
+
errors.push(`Invalid suite: expected "${expectedSuite}", got "${env.suite}"`);
|
|
278
286
|
}
|
|
279
287
|
if (env.aead !== ENVELOPE_AEAD) {
|
|
280
288
|
errors.push(`Invalid aead: expected "${ENVELOPE_AEAD}", got "${env.aead}"`);
|
|
281
289
|
}
|
|
282
|
-
const
|
|
283
|
-
for (const field of required) {
|
|
290
|
+
for (const field of requiredFields) {
|
|
284
291
|
if (!(field in env)) {
|
|
285
292
|
errors.push(`Missing required field: ${field}`);
|
|
286
293
|
}
|
|
@@ -375,7 +382,6 @@ async function generateHybridIdentity(name) {
|
|
|
375
382
|
const x25519 = generateX25519Keypair();
|
|
376
383
|
const kyber = await generateKyberKeypair();
|
|
377
384
|
if (!kyber) {
|
|
378
|
-
console.error("Kyber key generation failed - library not available");
|
|
379
385
|
return null;
|
|
380
386
|
}
|
|
381
387
|
return {
|
|
@@ -401,49 +407,74 @@ function getSecretKeys(identity) {
|
|
|
401
407
|
kyberSecB64: identity.kyberSecB64
|
|
402
408
|
};
|
|
403
409
|
}
|
|
410
|
+
function combinedKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
411
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
412
|
+
ikm.set(kyberShared, 0);
|
|
413
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
414
|
+
try {
|
|
415
|
+
return hkdfFlex(ikm, "omnituum/hybrid-v2", `wrap-ck|${x25519EpkHex}|${kyberKemCtB64}`);
|
|
416
|
+
} finally {
|
|
417
|
+
ikm.fill(0);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
404
420
|
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
405
421
|
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
406
422
|
const CK = rand32();
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
423
|
+
try {
|
|
424
|
+
const contentNonce = rand24();
|
|
425
|
+
const ciphertext = nacl4__default.default.secretbox(pt, contentNonce, CK);
|
|
426
|
+
const x25519EphKp = nacl4__default.default.box.keyPair();
|
|
427
|
+
const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
|
|
428
|
+
const x25519Shared = nacl4__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
429
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
430
|
+
const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
|
|
431
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
432
|
+
const kek = combinedKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
433
|
+
const ckWrapNonce = rand24();
|
|
434
|
+
const ckWrapped = nacl4__default.default.secretbox(CK, ckWrapNonce, kek);
|
|
435
|
+
kek.fill(0);
|
|
436
|
+
x25519Shared.fill(0);
|
|
437
|
+
kyberResult.sharedSecret.fill(0);
|
|
438
|
+
x25519EphKp.secretKey.fill(0);
|
|
439
|
+
return {
|
|
440
|
+
v: ENVELOPE_VERSION_V2,
|
|
441
|
+
suite: ENVELOPE_SUITE_V2,
|
|
442
|
+
aead: ENVELOPE_AEAD,
|
|
443
|
+
x25519Epk: x25519EpkHex,
|
|
444
|
+
kyberKemCt: kyberKemCtB64,
|
|
445
|
+
ckWrap: {
|
|
446
|
+
nonce: b64(ckWrapNonce),
|
|
447
|
+
wrapped: b64(ckWrapped)
|
|
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
|
+
};
|
|
457
|
+
} finally {
|
|
458
|
+
CK.fill(0);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function unwrapCkV2(envelope, secretKeys) {
|
|
462
|
+
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
463
|
+
const ephPk = fromHex(envelope.x25519Epk);
|
|
464
|
+
const sk = fromHex(secretKeys.x25519SecHex);
|
|
465
|
+
const x25519Shared = nacl4__default.default.scalarMult(sk, ephPk);
|
|
466
|
+
const kek = combinedKekV2(kyberShared, x25519Shared, envelope.x25519Epk, envelope.kyberKemCt);
|
|
467
|
+
kyberShared.fill(0);
|
|
468
|
+
x25519Shared.fill(0);
|
|
469
|
+
const CK = nacl4__default.default.secretbox.open(ub64(envelope.ckWrap.wrapped), ub64(envelope.ckWrap.nonce), kek);
|
|
470
|
+
kek.fill(0);
|
|
471
|
+
if (!CK) {
|
|
472
|
+
throw new Error("Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
473
|
+
}
|
|
474
|
+
return CK;
|
|
441
475
|
}
|
|
442
|
-
async function
|
|
443
|
-
const version = envelope.v;
|
|
444
|
-
assertEnvelopeVersion(version);
|
|
476
|
+
async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
|
|
445
477
|
let CK = null;
|
|
446
|
-
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
447
478
|
try {
|
|
448
479
|
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
449
480
|
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
@@ -452,11 +483,7 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
452
483
|
ub64(envelope.kyberWrap.nonce),
|
|
453
484
|
kyberKek
|
|
454
485
|
);
|
|
455
|
-
|
|
456
|
-
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
457
|
-
}
|
|
458
|
-
} catch (e) {
|
|
459
|
-
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
486
|
+
} catch {
|
|
460
487
|
}
|
|
461
488
|
if (!CK) {
|
|
462
489
|
try {
|
|
@@ -469,21 +496,28 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
469
496
|
ub64(envelope.x25519Wrap.nonce),
|
|
470
497
|
x25519Kek
|
|
471
498
|
);
|
|
472
|
-
|
|
473
|
-
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
474
|
-
}
|
|
475
|
-
} catch (e) {
|
|
476
|
-
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
499
|
+
} catch {
|
|
477
500
|
}
|
|
478
501
|
}
|
|
479
502
|
if (!CK) {
|
|
480
503
|
throw new Error("Could not unwrap content key with either algorithm");
|
|
481
504
|
}
|
|
505
|
+
return CK;
|
|
506
|
+
}
|
|
507
|
+
async function hybridDecrypt(envelope, secretKeys) {
|
|
508
|
+
const version = envelope.v;
|
|
509
|
+
assertEnvelopeVersion(version);
|
|
510
|
+
const CK = version === ENVELOPE_VERSION_V2 ? await unwrapCkV2(envelope, secretKeys) : await unwrapCkV1(
|
|
511
|
+
envelope,
|
|
512
|
+
secretKeys,
|
|
513
|
+
version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum"
|
|
514
|
+
);
|
|
482
515
|
const pt = nacl4__default.default.secretbox.open(
|
|
483
516
|
ub64(envelope.ciphertext),
|
|
484
517
|
ub64(envelope.contentNonce),
|
|
485
518
|
CK
|
|
486
519
|
);
|
|
520
|
+
CK.fill(0);
|
|
487
521
|
if (!pt) {
|
|
488
522
|
throw new Error("Content authentication failed");
|
|
489
523
|
}
|
|
@@ -502,8 +536,7 @@ async function loadDilithium() {
|
|
|
502
536
|
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
|
|
503
537
|
dilithiumModule = ml_dsa65;
|
|
504
538
|
return dilithiumModule;
|
|
505
|
-
} catch
|
|
506
|
-
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
539
|
+
} catch {
|
|
507
540
|
return null;
|
|
508
541
|
}
|
|
509
542
|
}
|
|
@@ -521,8 +554,7 @@ async function generateDilithiumKeypair() {
|
|
|
521
554
|
publicB64: toB64(kp.publicKey),
|
|
522
555
|
secretB64: toB64(kp.secretKey)
|
|
523
556
|
};
|
|
524
|
-
} catch
|
|
525
|
-
console.warn("[Dilithium] Key generation failed:", e);
|
|
557
|
+
} catch {
|
|
526
558
|
return null;
|
|
527
559
|
}
|
|
528
560
|
}
|
|
@@ -861,17 +893,11 @@ function computeIntegrityHash(identities) {
|
|
|
861
893
|
createdAt: i.createdAt,
|
|
862
894
|
rotationCount: i.rotationCount
|
|
863
895
|
}));
|
|
864
|
-
const serialized = JSON.stringify(canonical
|
|
896
|
+
const serialized = JSON.stringify(canonical);
|
|
865
897
|
return computeStringHash(serialized);
|
|
866
898
|
}
|
|
867
899
|
function computeStringHash(str) {
|
|
868
|
-
|
|
869
|
-
for (let i = 0; i < str.length; i++) {
|
|
870
|
-
const char = str.charCodeAt(i);
|
|
871
|
-
hash = (hash << 5) - hash + char;
|
|
872
|
-
hash = hash & hash;
|
|
873
|
-
}
|
|
874
|
-
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
900
|
+
return toHex(sha256(textEncoder.encode(str)));
|
|
875
901
|
}
|
|
876
902
|
async function computeHashAsync(data) {
|
|
877
903
|
const encoder = new TextEncoder();
|
|
@@ -1122,7 +1148,6 @@ async function createIdentity(name) {
|
|
|
1122
1148
|
const x25519 = generateX25519Keypair();
|
|
1123
1149
|
const kyber = await generateKyberKeypair();
|
|
1124
1150
|
if (!kyber) {
|
|
1125
|
-
console.error("Kyber key generation failed");
|
|
1126
1151
|
return null;
|
|
1127
1152
|
}
|
|
1128
1153
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1345,12 +1370,22 @@ async function migrateEncryptedVault(options) {
|
|
|
1345
1370
|
|
|
1346
1371
|
// src/fs/types.ts
|
|
1347
1372
|
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
1348
|
-
var
|
|
1373
|
+
var OQE_FORMAT_VERSION_V1 = 1;
|
|
1374
|
+
var OQE_FORMAT_VERSION_V2 = 2;
|
|
1375
|
+
var SUPPORTED_OQE_VERSIONS = [OQE_FORMAT_VERSION_V1, OQE_FORMAT_VERSION_V2];
|
|
1349
1376
|
var ALGORITHM_SUITES = {
|
|
1350
|
-
/**
|
|
1377
|
+
/**
|
|
1378
|
+
* LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
|
|
1379
|
+
* wraps (either secret unwraps). Only appears in v1 files. Never written.
|
|
1380
|
+
*/
|
|
1351
1381
|
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
1352
1382
|
/** Password: Argon2id + AES-256-GCM */
|
|
1353
|
-
PASSWORD_ARGON2ID_AES256GCM: 2
|
|
1383
|
+
PASSWORD_ARGON2ID_AES256GCM: 2,
|
|
1384
|
+
/**
|
|
1385
|
+
* Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
|
|
1386
|
+
* (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
|
|
1387
|
+
*/
|
|
1388
|
+
HYBRID_X25519_MLKEM1024_AES256GCM: 3
|
|
1354
1389
|
};
|
|
1355
1390
|
var DEFAULT_ARGON2ID_PARAMS = {
|
|
1356
1391
|
memoryCost: 65536,
|
|
@@ -1360,7 +1395,8 @@ var DEFAULT_ARGON2ID_PARAMS = {
|
|
|
1360
1395
|
hashLength: 32,
|
|
1361
1396
|
saltLength: 32
|
|
1362
1397
|
};
|
|
1363
|
-
var
|
|
1398
|
+
var OQE_HEADER_SIZE_V1 = 30;
|
|
1399
|
+
var OQE_HEADER_SIZE_V2 = 42;
|
|
1364
1400
|
var OQEError = class extends Error {
|
|
1365
1401
|
constructor(code, message) {
|
|
1366
1402
|
super(message);
|
|
@@ -1430,6 +1466,7 @@ function toArrayBuffer(arr) {
|
|
|
1430
1466
|
}
|
|
1431
1467
|
var AES_KEY_SIZE = 32;
|
|
1432
1468
|
var AES_GCM_IV_SIZE = 12;
|
|
1469
|
+
var AES_GCM_TAG_SIZE = 16;
|
|
1433
1470
|
async function importAesKey(keyBytes) {
|
|
1434
1471
|
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
1435
1472
|
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
@@ -1453,7 +1490,7 @@ async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
|
1453
1490
|
{
|
|
1454
1491
|
name: "AES-GCM",
|
|
1455
1492
|
iv: toArrayBuffer(ivBytes),
|
|
1456
|
-
additionalData: void 0,
|
|
1493
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
1457
1494
|
tagLength: 128
|
|
1458
1495
|
// 16 bytes
|
|
1459
1496
|
},
|
|
@@ -1489,6 +1526,9 @@ async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
|
1489
1526
|
}
|
|
1490
1527
|
|
|
1491
1528
|
// src/fs/format.ts
|
|
1529
|
+
function oqeHeaderSize(version) {
|
|
1530
|
+
return version === OQE_FORMAT_VERSION_V2 ? OQE_HEADER_SIZE_V2 : OQE_HEADER_SIZE_V1;
|
|
1531
|
+
}
|
|
1492
1532
|
function writeUint32BE(value) {
|
|
1493
1533
|
const buffer = new ArrayBuffer(4);
|
|
1494
1534
|
new DataView(buffer).setUint32(0, value, false);
|
|
@@ -1506,7 +1546,8 @@ function readUint16BE(data, offset) {
|
|
|
1506
1546
|
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
1507
1547
|
}
|
|
1508
1548
|
function writeOQEHeader(header) {
|
|
1509
|
-
const
|
|
1549
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
1550
|
+
const buffer = new Uint8Array(oqeHeaderSize(header.version));
|
|
1510
1551
|
let offset = 0;
|
|
1511
1552
|
buffer.set(OQE_MAGIC, offset);
|
|
1512
1553
|
offset += 4;
|
|
@@ -1519,11 +1560,18 @@ function writeOQEHeader(header) {
|
|
|
1519
1560
|
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
1520
1561
|
offset += 4;
|
|
1521
1562
|
buffer.set(header.iv, offset);
|
|
1563
|
+
offset += 12;
|
|
1564
|
+
if (isV2) {
|
|
1565
|
+
if (!header.contentIv || header.contentIv.length !== 12) {
|
|
1566
|
+
throw new OQEError("INVALID_HEADER", "v2 header requires a 12-byte contentIv");
|
|
1567
|
+
}
|
|
1568
|
+
buffer.set(header.contentIv, offset);
|
|
1569
|
+
}
|
|
1522
1570
|
return buffer;
|
|
1523
1571
|
}
|
|
1524
1572
|
function parseOQEHeader(data) {
|
|
1525
|
-
if (data.length <
|
|
1526
|
-
throw new OQEError("INVALID_HEADER", `File too small: need ${
|
|
1573
|
+
if (data.length < OQE_HEADER_SIZE_V1) {
|
|
1574
|
+
throw new OQEError("INVALID_HEADER", `File too small: need at least ${OQE_HEADER_SIZE_V1} bytes, got ${data.length}`);
|
|
1527
1575
|
}
|
|
1528
1576
|
let offset = 0;
|
|
1529
1577
|
const magic = data.slice(0, 4);
|
|
@@ -1532,14 +1580,19 @@ function parseOQEHeader(data) {
|
|
|
1532
1580
|
}
|
|
1533
1581
|
offset += 4;
|
|
1534
1582
|
const version = data[offset++];
|
|
1535
|
-
if (version
|
|
1583
|
+
if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
|
|
1536
1584
|
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
1537
1585
|
}
|
|
1538
1586
|
const algorithmSuiteRaw = data[offset++];
|
|
1539
|
-
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1587
|
+
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1540
1588
|
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
1541
1589
|
}
|
|
1542
1590
|
const algorithmSuite = algorithmSuiteRaw;
|
|
1591
|
+
const isV2 = version === OQE_FORMAT_VERSION_V2;
|
|
1592
|
+
const headerSize = oqeHeaderSize(version);
|
|
1593
|
+
if (data.length < headerSize) {
|
|
1594
|
+
throw new OQEError("INVALID_HEADER", `Truncated v${version} header: need ${headerSize} bytes, got ${data.length}`);
|
|
1595
|
+
}
|
|
1543
1596
|
const flags = readUint32BE(data, offset);
|
|
1544
1597
|
offset += 4;
|
|
1545
1598
|
const metadataLength = readUint32BE(data, offset);
|
|
@@ -1547,35 +1600,18 @@ function parseOQEHeader(data) {
|
|
|
1547
1600
|
const keyMaterialLength = readUint32BE(data, offset);
|
|
1548
1601
|
offset += 4;
|
|
1549
1602
|
const iv = data.slice(offset, offset + 12);
|
|
1603
|
+
offset += 12;
|
|
1604
|
+
const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
|
|
1550
1605
|
return {
|
|
1551
1606
|
version,
|
|
1552
1607
|
algorithmSuite,
|
|
1553
1608
|
flags,
|
|
1554
1609
|
metadataLength,
|
|
1555
1610
|
keyMaterialLength,
|
|
1556
|
-
iv
|
|
1611
|
+
iv,
|
|
1612
|
+
contentIv
|
|
1557
1613
|
};
|
|
1558
1614
|
}
|
|
1559
|
-
function serializeHybridKeyMaterial(km) {
|
|
1560
|
-
const parts = [];
|
|
1561
|
-
parts.push(km.x25519EphemeralPk);
|
|
1562
|
-
parts.push(km.x25519Nonce);
|
|
1563
|
-
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
1564
|
-
parts.push(km.x25519WrappedKey);
|
|
1565
|
-
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1566
|
-
parts.push(km.kyberCiphertext);
|
|
1567
|
-
parts.push(km.kyberNonce);
|
|
1568
|
-
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
1569
|
-
parts.push(km.kyberWrappedKey);
|
|
1570
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1571
|
-
const result = new Uint8Array(totalLength);
|
|
1572
|
-
let offset = 0;
|
|
1573
|
-
for (const part of parts) {
|
|
1574
|
-
result.set(part, offset);
|
|
1575
|
-
offset += part.length;
|
|
1576
|
-
}
|
|
1577
|
-
return result;
|
|
1578
|
-
}
|
|
1579
1615
|
function parseHybridKeyMaterial(data) {
|
|
1580
1616
|
let offset = 0;
|
|
1581
1617
|
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
@@ -1604,6 +1640,38 @@ function parseHybridKeyMaterial(data) {
|
|
|
1604
1640
|
kyberWrappedKey
|
|
1605
1641
|
};
|
|
1606
1642
|
}
|
|
1643
|
+
function serializeHybridKeyMaterialV2(km) {
|
|
1644
|
+
const parts = [];
|
|
1645
|
+
parts.push(km.x25519EphemeralPk);
|
|
1646
|
+
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1647
|
+
parts.push(km.kyberCiphertext);
|
|
1648
|
+
parts.push(km.ckWrapNonce);
|
|
1649
|
+
parts.push(writeUint16BE(km.ckWrapped.length));
|
|
1650
|
+
parts.push(km.ckWrapped);
|
|
1651
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1652
|
+
const result = new Uint8Array(totalLength);
|
|
1653
|
+
let offset = 0;
|
|
1654
|
+
for (const part of parts) {
|
|
1655
|
+
result.set(part, offset);
|
|
1656
|
+
offset += part.length;
|
|
1657
|
+
}
|
|
1658
|
+
return result;
|
|
1659
|
+
}
|
|
1660
|
+
function parseHybridKeyMaterialV2(data) {
|
|
1661
|
+
let offset = 0;
|
|
1662
|
+
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
1663
|
+
offset += 32;
|
|
1664
|
+
const kyberCtLen = readUint16BE(data, offset);
|
|
1665
|
+
offset += 2;
|
|
1666
|
+
const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
|
|
1667
|
+
offset += kyberCtLen;
|
|
1668
|
+
const ckWrapNonce = data.slice(offset, offset + 24);
|
|
1669
|
+
offset += 24;
|
|
1670
|
+
const ckWrappedLen = readUint16BE(data, offset);
|
|
1671
|
+
offset += 2;
|
|
1672
|
+
const ckWrapped = data.slice(offset, offset + ckWrappedLen);
|
|
1673
|
+
return { x25519EphemeralPk, kyberCiphertext, ckWrapNonce, ckWrapped };
|
|
1674
|
+
}
|
|
1607
1675
|
function serializePasswordKeyMaterial(km) {
|
|
1608
1676
|
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
1609
1677
|
result.set(km.salt, 0);
|
|
@@ -1645,7 +1713,7 @@ function assembleOQEFile(components) {
|
|
|
1645
1713
|
}
|
|
1646
1714
|
function parseOQEFile(data) {
|
|
1647
1715
|
const header = parseOQEHeader(data);
|
|
1648
|
-
let offset =
|
|
1716
|
+
let offset = oqeHeaderSize(header.version);
|
|
1649
1717
|
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
1650
1718
|
offset += header.keyMaterialLength;
|
|
1651
1719
|
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
@@ -1665,61 +1733,81 @@ function addOQEExtension(filename) {
|
|
|
1665
1733
|
}
|
|
1666
1734
|
return `${filename}${OQE_EXTENSION}`;
|
|
1667
1735
|
}
|
|
1668
|
-
|
|
1669
|
-
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1670
|
-
}
|
|
1736
|
+
var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
|
|
1671
1737
|
function computeIdentityHash(publicKeyHex) {
|
|
1672
1738
|
const hash = sha256(fromHex(publicKeyHex));
|
|
1673
1739
|
return toHex(hash).slice(0, 16);
|
|
1674
1740
|
}
|
|
1741
|
+
function combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
1742
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
1743
|
+
ikm.set(kyberShared, 0);
|
|
1744
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
1745
|
+
try {
|
|
1746
|
+
return hkdfSha256(ikm, {
|
|
1747
|
+
salt: u8("omnituum/fs/hybrid-v2"),
|
|
1748
|
+
info: u8(`wrap-content-key|${x25519EpkHex}|${kyberKemCtB64}`),
|
|
1749
|
+
length: 32
|
|
1750
|
+
});
|
|
1751
|
+
} finally {
|
|
1752
|
+
ikm.fill(0);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1675
1755
|
async function encryptHybrid(plaintext, metadata, options) {
|
|
1676
1756
|
if (!await isKyberAvailable()) {
|
|
1677
1757
|
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
1678
1758
|
}
|
|
1679
1759
|
const contentKey = rand32();
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
encryptedMetadata,
|
|
1715
|
-
encryptedContent
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1760
|
+
try {
|
|
1761
|
+
const metadataIv = rand12();
|
|
1762
|
+
const contentIv = rand12();
|
|
1763
|
+
const x25519EphKp = nacl4__default.default.box.keyPair();
|
|
1764
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
1765
|
+
const x25519Shared = nacl4__default.default.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
1766
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
1767
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
1768
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
1769
|
+
const kek = combinedFileKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
1770
|
+
const ckWrapNonce = rand24();
|
|
1771
|
+
const ckWrapped = nacl4__default.default.secretbox(contentKey, ckWrapNonce, kek);
|
|
1772
|
+
kek.fill(0);
|
|
1773
|
+
x25519Shared.fill(0);
|
|
1774
|
+
kyberResult.sharedSecret.fill(0);
|
|
1775
|
+
x25519EphKp.secretKey.fill(0);
|
|
1776
|
+
const keyMaterial = {
|
|
1777
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
1778
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
1779
|
+
ckWrapNonce,
|
|
1780
|
+
ckWrapped
|
|
1781
|
+
};
|
|
1782
|
+
const keyMaterialBytes = serializeHybridKeyMaterialV2(keyMaterial);
|
|
1783
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1784
|
+
const header = {
|
|
1785
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
1786
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM,
|
|
1787
|
+
flags: 0,
|
|
1788
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
1789
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1790
|
+
iv: metadataIv,
|
|
1791
|
+
contentIv
|
|
1792
|
+
};
|
|
1793
|
+
const aad = writeOQEHeader(header);
|
|
1794
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
1795
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
1796
|
+
const fileData = assembleOQEFile({
|
|
1797
|
+
header,
|
|
1798
|
+
keyMaterial: keyMaterialBytes,
|
|
1799
|
+
encryptedMetadata,
|
|
1800
|
+
encryptedContent
|
|
1801
|
+
});
|
|
1802
|
+
return {
|
|
1803
|
+
data: fileData,
|
|
1804
|
+
filename: addOQEExtension(metadata.filename),
|
|
1805
|
+
metadata,
|
|
1806
|
+
mode: "hybrid"
|
|
1807
|
+
};
|
|
1808
|
+
} finally {
|
|
1809
|
+
contentKey.fill(0);
|
|
1810
|
+
}
|
|
1723
1811
|
}
|
|
1724
1812
|
async function encryptPassword(plaintext, metadata, options) {
|
|
1725
1813
|
if (!await isArgon2Available()) {
|
|
@@ -1731,37 +1819,44 @@ async function encryptPassword(plaintext, metadata, options) {
|
|
|
1731
1819
|
};
|
|
1732
1820
|
const salt = generateArgon2Salt(params.saltLength);
|
|
1733
1821
|
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
header
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1822
|
+
try {
|
|
1823
|
+
const metadataIv = rand12();
|
|
1824
|
+
const contentIv = rand12();
|
|
1825
|
+
const keyMaterial = {
|
|
1826
|
+
salt,
|
|
1827
|
+
memoryCost: params.memoryCost,
|
|
1828
|
+
timeCost: params.timeCost,
|
|
1829
|
+
parallelism: params.parallelism
|
|
1830
|
+
};
|
|
1831
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
1832
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1833
|
+
const header = {
|
|
1834
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
1835
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
1836
|
+
flags: 0,
|
|
1837
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
1838
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1839
|
+
iv: metadataIv,
|
|
1840
|
+
contentIv
|
|
1841
|
+
};
|
|
1842
|
+
const aad = writeOQEHeader(header);
|
|
1843
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
1844
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
1845
|
+
const fileData = assembleOQEFile({
|
|
1846
|
+
header,
|
|
1847
|
+
keyMaterial: keyMaterialBytes,
|
|
1848
|
+
encryptedMetadata,
|
|
1849
|
+
encryptedContent
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
data: fileData,
|
|
1853
|
+
filename: addOQEExtension(metadata.filename),
|
|
1854
|
+
metadata,
|
|
1855
|
+
mode: "password"
|
|
1856
|
+
};
|
|
1857
|
+
} finally {
|
|
1858
|
+
contentKey.fill(0);
|
|
1859
|
+
}
|
|
1765
1860
|
}
|
|
1766
1861
|
async function encryptFile(input, options) {
|
|
1767
1862
|
const plaintext = await toUint8Array(input.data);
|
|
@@ -1789,17 +1884,56 @@ async function encryptFileWithPassword(input, password) {
|
|
|
1789
1884
|
password
|
|
1790
1885
|
});
|
|
1791
1886
|
}
|
|
1792
|
-
function
|
|
1887
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
1793
1888
|
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1794
1889
|
}
|
|
1890
|
+
function headerAad(header) {
|
|
1891
|
+
return writeOQEHeader(header);
|
|
1892
|
+
}
|
|
1795
1893
|
async function decryptHybrid(encryptedData, options) {
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1894
|
+
const parsed = parseOQEFile(encryptedData);
|
|
1895
|
+
const { header } = parsed;
|
|
1896
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
|
|
1897
|
+
return decryptHybridV2(parsed, options);
|
|
1898
|
+
}
|
|
1899
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
1900
|
+
return decryptHybridV1Legacy(parsed, options);
|
|
1802
1901
|
}
|
|
1902
|
+
throw new OQEError(
|
|
1903
|
+
"UNSUPPORTED_ALGORITHM",
|
|
1904
|
+
"This file was not encrypted with hybrid mode. Use password decryption."
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
async function decryptHybridV2(parsed, options) {
|
|
1908
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
1909
|
+
if (header.version !== OQE_FORMAT_VERSION_V2 || !header.contentIv) {
|
|
1910
|
+
throw new OQEError("INVALID_HEADER", "v2 hybrid file missing content IV");
|
|
1911
|
+
}
|
|
1912
|
+
const km = parseHybridKeyMaterialV2(keyMaterial);
|
|
1913
|
+
const x25519EpkHex = toHex(km.x25519EphemeralPk);
|
|
1914
|
+
const kyberKemCtB64 = b64(km.kyberCiphertext);
|
|
1915
|
+
const kyberShared = await kyberDecapsulate(kyberKemCtB64, options.recipientSecretKeys.kyberSecB64);
|
|
1916
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1917
|
+
const x25519Shared = nacl4__default.default.scalarMult(sk, km.x25519EphemeralPk);
|
|
1918
|
+
const kek = combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
1919
|
+
kyberShared.fill(0);
|
|
1920
|
+
x25519Shared.fill(0);
|
|
1921
|
+
const contentKey = nacl4__default.default.secretbox.open(km.ckWrapped, km.ckWrapNonce, kek);
|
|
1922
|
+
kek.fill(0);
|
|
1923
|
+
if (!contentKey) {
|
|
1924
|
+
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
1925
|
+
}
|
|
1926
|
+
const aad = headerAad(header);
|
|
1927
|
+
try {
|
|
1928
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, aad, "metadata");
|
|
1929
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.contentIv, aad);
|
|
1930
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
1931
|
+
} finally {
|
|
1932
|
+
contentKey.fill(0);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
async function decryptHybridV1Legacy(parsed, options) {
|
|
1936
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
1803
1937
|
const km = parseHybridKeyMaterial(keyMaterial);
|
|
1804
1938
|
let contentKey = null;
|
|
1805
1939
|
if (await isKyberAvailable()) {
|
|
@@ -1808,54 +1942,47 @@ async function decryptHybrid(encryptedData, options) {
|
|
|
1808
1942
|
toB64(km.kyberCiphertext),
|
|
1809
1943
|
options.recipientSecretKeys.kyberSecB64
|
|
1810
1944
|
);
|
|
1811
|
-
const kyberKek =
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
contentKey = unwrapped;
|
|
1815
|
-
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
1816
|
-
}
|
|
1817
|
-
} catch (e) {
|
|
1818
|
-
console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
|
|
1945
|
+
const kyberKek = hkdfFlex2(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
|
|
1946
|
+
contentKey = nacl4__default.default.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
1947
|
+
} catch {
|
|
1819
1948
|
}
|
|
1820
1949
|
}
|
|
1821
1950
|
if (!contentKey) {
|
|
1822
1951
|
try {
|
|
1823
|
-
const ephPk = km.x25519EphemeralPk;
|
|
1824
1952
|
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1825
|
-
const x25519Shared = nacl4__default.default.scalarMult(sk,
|
|
1826
|
-
const x25519Kek =
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
contentKey = unwrapped;
|
|
1830
|
-
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
1831
|
-
}
|
|
1832
|
-
} catch (e) {
|
|
1833
|
-
console.warn("[OQE] X25519 decryption failed:", e);
|
|
1953
|
+
const x25519Shared = nacl4__default.default.scalarMult(sk, km.x25519EphemeralPk);
|
|
1954
|
+
const x25519Kek = hkdfFlex2(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
1955
|
+
contentKey = nacl4__default.default.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
1956
|
+
} catch {
|
|
1834
1957
|
}
|
|
1835
1958
|
}
|
|
1836
1959
|
if (!contentKey) {
|
|
1837
1960
|
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
1838
1961
|
}
|
|
1839
|
-
let metadata;
|
|
1840
1962
|
try {
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1963
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, void 0, "metadata");
|
|
1964
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
1965
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
1966
|
+
} finally {
|
|
1967
|
+
contentKey.fill(0);
|
|
1845
1968
|
}
|
|
1846
|
-
|
|
1969
|
+
}
|
|
1970
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
1847
1971
|
try {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1972
|
+
const bytes = await aesDecrypt(ciphertext, key, iv, aad);
|
|
1973
|
+
return parseMetadata(bytes);
|
|
1974
|
+
} catch {
|
|
1975
|
+
throw new OQEError("DECRYPTION_FAILED", `Failed to decrypt file ${what}`);
|
|
1851
1976
|
}
|
|
1977
|
+
}
|
|
1978
|
+
function buildResult(plaintext, metadata, mode) {
|
|
1852
1979
|
return {
|
|
1853
1980
|
data: plaintext,
|
|
1854
1981
|
filename: metadata.filename,
|
|
1855
1982
|
mimeType: metadata.mimeType,
|
|
1856
1983
|
originalSize: metadata.originalSize,
|
|
1857
1984
|
metadata,
|
|
1858
|
-
mode
|
|
1985
|
+
mode
|
|
1859
1986
|
};
|
|
1860
1987
|
}
|
|
1861
1988
|
async function decryptPassword(encryptedData, options) {
|
|
@@ -1877,27 +2004,27 @@ async function decryptPassword(encryptedData, options) {
|
|
|
1877
2004
|
hashLength: 32,
|
|
1878
2005
|
saltLength: km.salt.length
|
|
1879
2006
|
});
|
|
1880
|
-
|
|
2007
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
2008
|
+
const aad = isV2 ? headerAad(header) : void 0;
|
|
2009
|
+
const contentIv = isV2 && header.contentIv ? header.contentIv : header.iv;
|
|
1881
2010
|
try {
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
plaintext
|
|
1890
|
-
|
|
1891
|
-
|
|
2011
|
+
let metadata;
|
|
2012
|
+
try {
|
|
2013
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv, aad);
|
|
2014
|
+
metadata = parseMetadata(metadataBytes);
|
|
2015
|
+
} catch {
|
|
2016
|
+
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
2017
|
+
}
|
|
2018
|
+
let plaintext;
|
|
2019
|
+
try {
|
|
2020
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, contentIv, aad);
|
|
2021
|
+
} catch {
|
|
2022
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
2023
|
+
}
|
|
2024
|
+
return buildResult(plaintext, metadata, "password");
|
|
2025
|
+
} finally {
|
|
2026
|
+
contentKey.fill(0);
|
|
1892
2027
|
}
|
|
1893
|
-
return {
|
|
1894
|
-
data: plaintext,
|
|
1895
|
-
filename: metadata.filename,
|
|
1896
|
-
mimeType: metadata.mimeType,
|
|
1897
|
-
originalSize: metadata.originalSize,
|
|
1898
|
-
metadata,
|
|
1899
|
-
mode: "password"
|
|
1900
|
-
};
|
|
1901
2028
|
}
|
|
1902
2029
|
async function decryptFile(encryptedData, options) {
|
|
1903
2030
|
const data = await toUint8Array(encryptedData);
|
|
@@ -2004,7 +2131,9 @@ exports.DILITHIUM_SECRET_KEY_SIZE = DILITHIUM_SECRET_KEY_SIZE;
|
|
|
2004
2131
|
exports.DILITHIUM_SIGNATURE_SIZE = DILITHIUM_SIGNATURE_SIZE;
|
|
2005
2132
|
exports.ENVELOPE_AEAD = ENVELOPE_AEAD;
|
|
2006
2133
|
exports.ENVELOPE_SUITE = ENVELOPE_SUITE;
|
|
2134
|
+
exports.ENVELOPE_SUITE_V2 = ENVELOPE_SUITE_V2;
|
|
2007
2135
|
exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
|
|
2136
|
+
exports.ENVELOPE_VERSION_V2 = ENVELOPE_VERSION_V2;
|
|
2008
2137
|
exports.KDF_CONFIG_ARGON2ID = KDF_CONFIG_ARGON2ID;
|
|
2009
2138
|
exports.KDF_CONFIG_PBKDF2 = KDF_CONFIG_PBKDF2;
|
|
2010
2139
|
exports.KYBER_CIPHERTEXT_SIZE = KYBER_CIPHERTEXT_SIZE;
|