@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.js
CHANGED
|
@@ -201,17 +201,20 @@ function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
|
201
201
|
return nacl4.secretbox.open(wrapped, nonce, kek) || null;
|
|
202
202
|
}
|
|
203
203
|
var ENVELOPE_VERSION = OMNI_VERSIONS.HYBRID_V1;
|
|
204
|
+
var ENVELOPE_VERSION_V2 = OMNI_VERSIONS.HYBRID_V2;
|
|
204
205
|
var ENVELOPE_VERSION_LEGACY = DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
|
|
205
206
|
var VAULT_VERSION = "omnituum.vault.v1";
|
|
206
207
|
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
207
208
|
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
208
209
|
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
210
|
+
var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
|
|
209
211
|
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
210
212
|
var VAULT_KDF = "PBKDF2-SHA256";
|
|
211
213
|
var VAULT_KDF_V2 = "Argon2id";
|
|
212
214
|
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
213
215
|
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
214
216
|
ENVELOPE_VERSION,
|
|
217
|
+
ENVELOPE_VERSION_V2,
|
|
215
218
|
ENVELOPE_VERSION_LEGACY
|
|
216
219
|
];
|
|
217
220
|
var SUPPORTED_VAULT_VERSIONS = [
|
|
@@ -256,6 +259,8 @@ function isVaultVersionSupported(version) {
|
|
|
256
259
|
function isVaultEncryptedVersionSupported(version) {
|
|
257
260
|
return SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version);
|
|
258
261
|
}
|
|
262
|
+
var ENVELOPE_V1_FIELDS = ["x25519Epk", "x25519Wrap", "kyberKemCt", "kyberWrap", "contentNonce", "ciphertext", "meta"];
|
|
263
|
+
var ENVELOPE_V2_FIELDS = ["x25519Epk", "kyberKemCt", "ckWrap", "contentNonce", "ciphertext", "meta"];
|
|
259
264
|
function validateEnvelope(envelope) {
|
|
260
265
|
const errors = [];
|
|
261
266
|
if (!envelope || typeof envelope !== "object") {
|
|
@@ -267,14 +272,16 @@ function validateEnvelope(envelope) {
|
|
|
267
272
|
} else if (!isEnvelopeVersionSupported(env.v)) {
|
|
268
273
|
errors.push(`Unsupported envelope version: ${env.v}`);
|
|
269
274
|
}
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
const isV2 = env.v === ENVELOPE_VERSION_V2;
|
|
276
|
+
const expectedSuite = isV2 ? ENVELOPE_SUITE_V2 : ENVELOPE_SUITE;
|
|
277
|
+
const requiredFields = isV2 ? ENVELOPE_V2_FIELDS : ENVELOPE_V1_FIELDS;
|
|
278
|
+
if (env.suite !== expectedSuite) {
|
|
279
|
+
errors.push(`Invalid suite: expected "${expectedSuite}", got "${env.suite}"`);
|
|
272
280
|
}
|
|
273
281
|
if (env.aead !== ENVELOPE_AEAD) {
|
|
274
282
|
errors.push(`Invalid aead: expected "${ENVELOPE_AEAD}", got "${env.aead}"`);
|
|
275
283
|
}
|
|
276
|
-
const
|
|
277
|
-
for (const field of required) {
|
|
284
|
+
for (const field of requiredFields) {
|
|
278
285
|
if (!(field in env)) {
|
|
279
286
|
errors.push(`Missing required field: ${field}`);
|
|
280
287
|
}
|
|
@@ -369,7 +376,6 @@ async function generateHybridIdentity(name) {
|
|
|
369
376
|
const x25519 = generateX25519Keypair();
|
|
370
377
|
const kyber = await generateKyberKeypair();
|
|
371
378
|
if (!kyber) {
|
|
372
|
-
console.error("Kyber key generation failed - library not available");
|
|
373
379
|
return null;
|
|
374
380
|
}
|
|
375
381
|
return {
|
|
@@ -395,49 +401,74 @@ function getSecretKeys(identity) {
|
|
|
395
401
|
kyberSecB64: identity.kyberSecB64
|
|
396
402
|
};
|
|
397
403
|
}
|
|
404
|
+
function combinedKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
405
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
406
|
+
ikm.set(kyberShared, 0);
|
|
407
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
408
|
+
try {
|
|
409
|
+
return hkdfFlex(ikm, "omnituum/hybrid-v2", `wrap-ck|${x25519EpkHex}|${kyberKemCtB64}`);
|
|
410
|
+
} finally {
|
|
411
|
+
ikm.fill(0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
398
414
|
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
399
415
|
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
400
416
|
const CK = rand32();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
417
|
+
try {
|
|
418
|
+
const contentNonce = rand24();
|
|
419
|
+
const ciphertext = nacl4.secretbox(pt, contentNonce, CK);
|
|
420
|
+
const x25519EphKp = nacl4.box.keyPair();
|
|
421
|
+
const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
|
|
422
|
+
const x25519Shared = nacl4.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
423
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
424
|
+
const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
|
|
425
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
426
|
+
const kek = combinedKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
427
|
+
const ckWrapNonce = rand24();
|
|
428
|
+
const ckWrapped = nacl4.secretbox(CK, ckWrapNonce, kek);
|
|
429
|
+
kek.fill(0);
|
|
430
|
+
x25519Shared.fill(0);
|
|
431
|
+
kyberResult.sharedSecret.fill(0);
|
|
432
|
+
x25519EphKp.secretKey.fill(0);
|
|
433
|
+
return {
|
|
434
|
+
v: ENVELOPE_VERSION_V2,
|
|
435
|
+
suite: ENVELOPE_SUITE_V2,
|
|
436
|
+
aead: ENVELOPE_AEAD,
|
|
437
|
+
x25519Epk: x25519EpkHex,
|
|
438
|
+
kyberKemCt: kyberKemCtB64,
|
|
439
|
+
ckWrap: {
|
|
440
|
+
nonce: b64(ckWrapNonce),
|
|
441
|
+
wrapped: b64(ckWrapped)
|
|
442
|
+
},
|
|
443
|
+
contentNonce: b64(contentNonce),
|
|
444
|
+
ciphertext: b64(ciphertext),
|
|
445
|
+
meta: {
|
|
446
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
447
|
+
senderName: sender?.name,
|
|
448
|
+
senderId: sender?.id
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
} finally {
|
|
452
|
+
CK.fill(0);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function unwrapCkV2(envelope, secretKeys) {
|
|
456
|
+
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
457
|
+
const ephPk = fromHex(envelope.x25519Epk);
|
|
458
|
+
const sk = fromHex(secretKeys.x25519SecHex);
|
|
459
|
+
const x25519Shared = nacl4.scalarMult(sk, ephPk);
|
|
460
|
+
const kek = combinedKekV2(kyberShared, x25519Shared, envelope.x25519Epk, envelope.kyberKemCt);
|
|
461
|
+
kyberShared.fill(0);
|
|
462
|
+
x25519Shared.fill(0);
|
|
463
|
+
const CK = nacl4.secretbox.open(ub64(envelope.ckWrap.wrapped), ub64(envelope.ckWrap.nonce), kek);
|
|
464
|
+
kek.fill(0);
|
|
465
|
+
if (!CK) {
|
|
466
|
+
throw new Error("Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
467
|
+
}
|
|
468
|
+
return CK;
|
|
435
469
|
}
|
|
436
|
-
async function
|
|
437
|
-
const version = envelope.v;
|
|
438
|
-
assertEnvelopeVersion(version);
|
|
470
|
+
async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
|
|
439
471
|
let CK = null;
|
|
440
|
-
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
441
472
|
try {
|
|
442
473
|
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
443
474
|
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
@@ -446,11 +477,7 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
446
477
|
ub64(envelope.kyberWrap.nonce),
|
|
447
478
|
kyberKek
|
|
448
479
|
);
|
|
449
|
-
|
|
450
|
-
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
451
|
-
}
|
|
452
|
-
} catch (e) {
|
|
453
|
-
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
480
|
+
} catch {
|
|
454
481
|
}
|
|
455
482
|
if (!CK) {
|
|
456
483
|
try {
|
|
@@ -463,21 +490,28 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
463
490
|
ub64(envelope.x25519Wrap.nonce),
|
|
464
491
|
x25519Kek
|
|
465
492
|
);
|
|
466
|
-
|
|
467
|
-
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
468
|
-
}
|
|
469
|
-
} catch (e) {
|
|
470
|
-
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
493
|
+
} catch {
|
|
471
494
|
}
|
|
472
495
|
}
|
|
473
496
|
if (!CK) {
|
|
474
497
|
throw new Error("Could not unwrap content key with either algorithm");
|
|
475
498
|
}
|
|
499
|
+
return CK;
|
|
500
|
+
}
|
|
501
|
+
async function hybridDecrypt(envelope, secretKeys) {
|
|
502
|
+
const version = envelope.v;
|
|
503
|
+
assertEnvelopeVersion(version);
|
|
504
|
+
const CK = version === ENVELOPE_VERSION_V2 ? await unwrapCkV2(envelope, secretKeys) : await unwrapCkV1(
|
|
505
|
+
envelope,
|
|
506
|
+
secretKeys,
|
|
507
|
+
version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum"
|
|
508
|
+
);
|
|
476
509
|
const pt = nacl4.secretbox.open(
|
|
477
510
|
ub64(envelope.ciphertext),
|
|
478
511
|
ub64(envelope.contentNonce),
|
|
479
512
|
CK
|
|
480
513
|
);
|
|
514
|
+
CK.fill(0);
|
|
481
515
|
if (!pt) {
|
|
482
516
|
throw new Error("Content authentication failed");
|
|
483
517
|
}
|
|
@@ -496,8 +530,7 @@ async function loadDilithium() {
|
|
|
496
530
|
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
|
|
497
531
|
dilithiumModule = ml_dsa65;
|
|
498
532
|
return dilithiumModule;
|
|
499
|
-
} catch
|
|
500
|
-
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
533
|
+
} catch {
|
|
501
534
|
return null;
|
|
502
535
|
}
|
|
503
536
|
}
|
|
@@ -515,8 +548,7 @@ async function generateDilithiumKeypair() {
|
|
|
515
548
|
publicB64: toB64(kp.publicKey),
|
|
516
549
|
secretB64: toB64(kp.secretKey)
|
|
517
550
|
};
|
|
518
|
-
} catch
|
|
519
|
-
console.warn("[Dilithium] Key generation failed:", e);
|
|
551
|
+
} catch {
|
|
520
552
|
return null;
|
|
521
553
|
}
|
|
522
554
|
}
|
|
@@ -855,17 +887,11 @@ function computeIntegrityHash(identities) {
|
|
|
855
887
|
createdAt: i.createdAt,
|
|
856
888
|
rotationCount: i.rotationCount
|
|
857
889
|
}));
|
|
858
|
-
const serialized = JSON.stringify(canonical
|
|
890
|
+
const serialized = JSON.stringify(canonical);
|
|
859
891
|
return computeStringHash(serialized);
|
|
860
892
|
}
|
|
861
893
|
function computeStringHash(str) {
|
|
862
|
-
|
|
863
|
-
for (let i = 0; i < str.length; i++) {
|
|
864
|
-
const char = str.charCodeAt(i);
|
|
865
|
-
hash = (hash << 5) - hash + char;
|
|
866
|
-
hash = hash & hash;
|
|
867
|
-
}
|
|
868
|
-
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
894
|
+
return toHex(sha256(textEncoder.encode(str)));
|
|
869
895
|
}
|
|
870
896
|
async function computeHashAsync(data) {
|
|
871
897
|
const encoder = new TextEncoder();
|
|
@@ -1116,7 +1142,6 @@ async function createIdentity(name) {
|
|
|
1116
1142
|
const x25519 = generateX25519Keypair();
|
|
1117
1143
|
const kyber = await generateKyberKeypair();
|
|
1118
1144
|
if (!kyber) {
|
|
1119
|
-
console.error("Kyber key generation failed");
|
|
1120
1145
|
return null;
|
|
1121
1146
|
}
|
|
1122
1147
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1339,12 +1364,22 @@ async function migrateEncryptedVault(options) {
|
|
|
1339
1364
|
|
|
1340
1365
|
// src/fs/types.ts
|
|
1341
1366
|
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
1342
|
-
var
|
|
1367
|
+
var OQE_FORMAT_VERSION_V1 = 1;
|
|
1368
|
+
var OQE_FORMAT_VERSION_V2 = 2;
|
|
1369
|
+
var SUPPORTED_OQE_VERSIONS = [OQE_FORMAT_VERSION_V1, OQE_FORMAT_VERSION_V2];
|
|
1343
1370
|
var ALGORITHM_SUITES = {
|
|
1344
|
-
/**
|
|
1371
|
+
/**
|
|
1372
|
+
* LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
|
|
1373
|
+
* wraps (either secret unwraps). Only appears in v1 files. Never written.
|
|
1374
|
+
*/
|
|
1345
1375
|
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
1346
1376
|
/** Password: Argon2id + AES-256-GCM */
|
|
1347
|
-
PASSWORD_ARGON2ID_AES256GCM: 2
|
|
1377
|
+
PASSWORD_ARGON2ID_AES256GCM: 2,
|
|
1378
|
+
/**
|
|
1379
|
+
* Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
|
|
1380
|
+
* (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
|
|
1381
|
+
*/
|
|
1382
|
+
HYBRID_X25519_MLKEM1024_AES256GCM: 3
|
|
1348
1383
|
};
|
|
1349
1384
|
var DEFAULT_ARGON2ID_PARAMS = {
|
|
1350
1385
|
memoryCost: 65536,
|
|
@@ -1354,7 +1389,8 @@ var DEFAULT_ARGON2ID_PARAMS = {
|
|
|
1354
1389
|
hashLength: 32,
|
|
1355
1390
|
saltLength: 32
|
|
1356
1391
|
};
|
|
1357
|
-
var
|
|
1392
|
+
var OQE_HEADER_SIZE_V1 = 30;
|
|
1393
|
+
var OQE_HEADER_SIZE_V2 = 42;
|
|
1358
1394
|
var OQEError = class extends Error {
|
|
1359
1395
|
constructor(code, message) {
|
|
1360
1396
|
super(message);
|
|
@@ -1424,6 +1460,7 @@ function toArrayBuffer(arr) {
|
|
|
1424
1460
|
}
|
|
1425
1461
|
var AES_KEY_SIZE = 32;
|
|
1426
1462
|
var AES_GCM_IV_SIZE = 12;
|
|
1463
|
+
var AES_GCM_TAG_SIZE = 16;
|
|
1427
1464
|
async function importAesKey(keyBytes) {
|
|
1428
1465
|
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
1429
1466
|
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
@@ -1447,7 +1484,7 @@ async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
|
1447
1484
|
{
|
|
1448
1485
|
name: "AES-GCM",
|
|
1449
1486
|
iv: toArrayBuffer(ivBytes),
|
|
1450
|
-
additionalData: void 0,
|
|
1487
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
1451
1488
|
tagLength: 128
|
|
1452
1489
|
// 16 bytes
|
|
1453
1490
|
},
|
|
@@ -1483,6 +1520,9 @@ async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
|
1483
1520
|
}
|
|
1484
1521
|
|
|
1485
1522
|
// src/fs/format.ts
|
|
1523
|
+
function oqeHeaderSize(version) {
|
|
1524
|
+
return version === OQE_FORMAT_VERSION_V2 ? OQE_HEADER_SIZE_V2 : OQE_HEADER_SIZE_V1;
|
|
1525
|
+
}
|
|
1486
1526
|
function writeUint32BE(value) {
|
|
1487
1527
|
const buffer = new ArrayBuffer(4);
|
|
1488
1528
|
new DataView(buffer).setUint32(0, value, false);
|
|
@@ -1500,7 +1540,8 @@ function readUint16BE(data, offset) {
|
|
|
1500
1540
|
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
1501
1541
|
}
|
|
1502
1542
|
function writeOQEHeader(header) {
|
|
1503
|
-
const
|
|
1543
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
1544
|
+
const buffer = new Uint8Array(oqeHeaderSize(header.version));
|
|
1504
1545
|
let offset = 0;
|
|
1505
1546
|
buffer.set(OQE_MAGIC, offset);
|
|
1506
1547
|
offset += 4;
|
|
@@ -1513,11 +1554,18 @@ function writeOQEHeader(header) {
|
|
|
1513
1554
|
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
1514
1555
|
offset += 4;
|
|
1515
1556
|
buffer.set(header.iv, offset);
|
|
1557
|
+
offset += 12;
|
|
1558
|
+
if (isV2) {
|
|
1559
|
+
if (!header.contentIv || header.contentIv.length !== 12) {
|
|
1560
|
+
throw new OQEError("INVALID_HEADER", "v2 header requires a 12-byte contentIv");
|
|
1561
|
+
}
|
|
1562
|
+
buffer.set(header.contentIv, offset);
|
|
1563
|
+
}
|
|
1516
1564
|
return buffer;
|
|
1517
1565
|
}
|
|
1518
1566
|
function parseOQEHeader(data) {
|
|
1519
|
-
if (data.length <
|
|
1520
|
-
throw new OQEError("INVALID_HEADER", `File too small: need ${
|
|
1567
|
+
if (data.length < OQE_HEADER_SIZE_V1) {
|
|
1568
|
+
throw new OQEError("INVALID_HEADER", `File too small: need at least ${OQE_HEADER_SIZE_V1} bytes, got ${data.length}`);
|
|
1521
1569
|
}
|
|
1522
1570
|
let offset = 0;
|
|
1523
1571
|
const magic = data.slice(0, 4);
|
|
@@ -1526,14 +1574,19 @@ function parseOQEHeader(data) {
|
|
|
1526
1574
|
}
|
|
1527
1575
|
offset += 4;
|
|
1528
1576
|
const version = data[offset++];
|
|
1529
|
-
if (version
|
|
1577
|
+
if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
|
|
1530
1578
|
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
1531
1579
|
}
|
|
1532
1580
|
const algorithmSuiteRaw = data[offset++];
|
|
1533
|
-
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1581
|
+
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1534
1582
|
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
1535
1583
|
}
|
|
1536
1584
|
const algorithmSuite = algorithmSuiteRaw;
|
|
1585
|
+
const isV2 = version === OQE_FORMAT_VERSION_V2;
|
|
1586
|
+
const headerSize = oqeHeaderSize(version);
|
|
1587
|
+
if (data.length < headerSize) {
|
|
1588
|
+
throw new OQEError("INVALID_HEADER", `Truncated v${version} header: need ${headerSize} bytes, got ${data.length}`);
|
|
1589
|
+
}
|
|
1537
1590
|
const flags = readUint32BE(data, offset);
|
|
1538
1591
|
offset += 4;
|
|
1539
1592
|
const metadataLength = readUint32BE(data, offset);
|
|
@@ -1541,35 +1594,18 @@ function parseOQEHeader(data) {
|
|
|
1541
1594
|
const keyMaterialLength = readUint32BE(data, offset);
|
|
1542
1595
|
offset += 4;
|
|
1543
1596
|
const iv = data.slice(offset, offset + 12);
|
|
1597
|
+
offset += 12;
|
|
1598
|
+
const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
|
|
1544
1599
|
return {
|
|
1545
1600
|
version,
|
|
1546
1601
|
algorithmSuite,
|
|
1547
1602
|
flags,
|
|
1548
1603
|
metadataLength,
|
|
1549
1604
|
keyMaterialLength,
|
|
1550
|
-
iv
|
|
1605
|
+
iv,
|
|
1606
|
+
contentIv
|
|
1551
1607
|
};
|
|
1552
1608
|
}
|
|
1553
|
-
function serializeHybridKeyMaterial(km) {
|
|
1554
|
-
const parts = [];
|
|
1555
|
-
parts.push(km.x25519EphemeralPk);
|
|
1556
|
-
parts.push(km.x25519Nonce);
|
|
1557
|
-
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
1558
|
-
parts.push(km.x25519WrappedKey);
|
|
1559
|
-
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1560
|
-
parts.push(km.kyberCiphertext);
|
|
1561
|
-
parts.push(km.kyberNonce);
|
|
1562
|
-
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
1563
|
-
parts.push(km.kyberWrappedKey);
|
|
1564
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1565
|
-
const result = new Uint8Array(totalLength);
|
|
1566
|
-
let offset = 0;
|
|
1567
|
-
for (const part of parts) {
|
|
1568
|
-
result.set(part, offset);
|
|
1569
|
-
offset += part.length;
|
|
1570
|
-
}
|
|
1571
|
-
return result;
|
|
1572
|
-
}
|
|
1573
1609
|
function parseHybridKeyMaterial(data) {
|
|
1574
1610
|
let offset = 0;
|
|
1575
1611
|
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
@@ -1598,6 +1634,38 @@ function parseHybridKeyMaterial(data) {
|
|
|
1598
1634
|
kyberWrappedKey
|
|
1599
1635
|
};
|
|
1600
1636
|
}
|
|
1637
|
+
function serializeHybridKeyMaterialV2(km) {
|
|
1638
|
+
const parts = [];
|
|
1639
|
+
parts.push(km.x25519EphemeralPk);
|
|
1640
|
+
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1641
|
+
parts.push(km.kyberCiphertext);
|
|
1642
|
+
parts.push(km.ckWrapNonce);
|
|
1643
|
+
parts.push(writeUint16BE(km.ckWrapped.length));
|
|
1644
|
+
parts.push(km.ckWrapped);
|
|
1645
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1646
|
+
const result = new Uint8Array(totalLength);
|
|
1647
|
+
let offset = 0;
|
|
1648
|
+
for (const part of parts) {
|
|
1649
|
+
result.set(part, offset);
|
|
1650
|
+
offset += part.length;
|
|
1651
|
+
}
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
function parseHybridKeyMaterialV2(data) {
|
|
1655
|
+
let offset = 0;
|
|
1656
|
+
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
1657
|
+
offset += 32;
|
|
1658
|
+
const kyberCtLen = readUint16BE(data, offset);
|
|
1659
|
+
offset += 2;
|
|
1660
|
+
const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
|
|
1661
|
+
offset += kyberCtLen;
|
|
1662
|
+
const ckWrapNonce = data.slice(offset, offset + 24);
|
|
1663
|
+
offset += 24;
|
|
1664
|
+
const ckWrappedLen = readUint16BE(data, offset);
|
|
1665
|
+
offset += 2;
|
|
1666
|
+
const ckWrapped = data.slice(offset, offset + ckWrappedLen);
|
|
1667
|
+
return { x25519EphemeralPk, kyberCiphertext, ckWrapNonce, ckWrapped };
|
|
1668
|
+
}
|
|
1601
1669
|
function serializePasswordKeyMaterial(km) {
|
|
1602
1670
|
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
1603
1671
|
result.set(km.salt, 0);
|
|
@@ -1639,7 +1707,7 @@ function assembleOQEFile(components) {
|
|
|
1639
1707
|
}
|
|
1640
1708
|
function parseOQEFile(data) {
|
|
1641
1709
|
const header = parseOQEHeader(data);
|
|
1642
|
-
let offset =
|
|
1710
|
+
let offset = oqeHeaderSize(header.version);
|
|
1643
1711
|
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
1644
1712
|
offset += header.keyMaterialLength;
|
|
1645
1713
|
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
@@ -1659,61 +1727,81 @@ function addOQEExtension(filename) {
|
|
|
1659
1727
|
}
|
|
1660
1728
|
return `${filename}${OQE_EXTENSION}`;
|
|
1661
1729
|
}
|
|
1662
|
-
|
|
1663
|
-
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1664
|
-
}
|
|
1730
|
+
var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
|
|
1665
1731
|
function computeIdentityHash(publicKeyHex) {
|
|
1666
1732
|
const hash = sha256(fromHex(publicKeyHex));
|
|
1667
1733
|
return toHex(hash).slice(0, 16);
|
|
1668
1734
|
}
|
|
1735
|
+
function combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
1736
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
1737
|
+
ikm.set(kyberShared, 0);
|
|
1738
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
1739
|
+
try {
|
|
1740
|
+
return hkdfSha256(ikm, {
|
|
1741
|
+
salt: u8("omnituum/fs/hybrid-v2"),
|
|
1742
|
+
info: u8(`wrap-content-key|${x25519EpkHex}|${kyberKemCtB64}`),
|
|
1743
|
+
length: 32
|
|
1744
|
+
});
|
|
1745
|
+
} finally {
|
|
1746
|
+
ikm.fill(0);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1669
1749
|
async function encryptHybrid(plaintext, metadata, options) {
|
|
1670
1750
|
if (!await isKyberAvailable()) {
|
|
1671
1751
|
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
1672
1752
|
}
|
|
1673
1753
|
const contentKey = rand32();
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
encryptedMetadata,
|
|
1709
|
-
encryptedContent
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1754
|
+
try {
|
|
1755
|
+
const metadataIv = rand12();
|
|
1756
|
+
const contentIv = rand12();
|
|
1757
|
+
const x25519EphKp = nacl4.box.keyPair();
|
|
1758
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
1759
|
+
const x25519Shared = nacl4.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
1760
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
1761
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
1762
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
1763
|
+
const kek = combinedFileKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
1764
|
+
const ckWrapNonce = rand24();
|
|
1765
|
+
const ckWrapped = nacl4.secretbox(contentKey, ckWrapNonce, kek);
|
|
1766
|
+
kek.fill(0);
|
|
1767
|
+
x25519Shared.fill(0);
|
|
1768
|
+
kyberResult.sharedSecret.fill(0);
|
|
1769
|
+
x25519EphKp.secretKey.fill(0);
|
|
1770
|
+
const keyMaterial = {
|
|
1771
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
1772
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
1773
|
+
ckWrapNonce,
|
|
1774
|
+
ckWrapped
|
|
1775
|
+
};
|
|
1776
|
+
const keyMaterialBytes = serializeHybridKeyMaterialV2(keyMaterial);
|
|
1777
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1778
|
+
const header = {
|
|
1779
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
1780
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM,
|
|
1781
|
+
flags: 0,
|
|
1782
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
1783
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1784
|
+
iv: metadataIv,
|
|
1785
|
+
contentIv
|
|
1786
|
+
};
|
|
1787
|
+
const aad = writeOQEHeader(header);
|
|
1788
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
1789
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
1790
|
+
const fileData = assembleOQEFile({
|
|
1791
|
+
header,
|
|
1792
|
+
keyMaterial: keyMaterialBytes,
|
|
1793
|
+
encryptedMetadata,
|
|
1794
|
+
encryptedContent
|
|
1795
|
+
});
|
|
1796
|
+
return {
|
|
1797
|
+
data: fileData,
|
|
1798
|
+
filename: addOQEExtension(metadata.filename),
|
|
1799
|
+
metadata,
|
|
1800
|
+
mode: "hybrid"
|
|
1801
|
+
};
|
|
1802
|
+
} finally {
|
|
1803
|
+
contentKey.fill(0);
|
|
1804
|
+
}
|
|
1717
1805
|
}
|
|
1718
1806
|
async function encryptPassword(plaintext, metadata, options) {
|
|
1719
1807
|
if (!await isArgon2Available()) {
|
|
@@ -1725,37 +1813,44 @@ async function encryptPassword(plaintext, metadata, options) {
|
|
|
1725
1813
|
};
|
|
1726
1814
|
const salt = generateArgon2Salt(params.saltLength);
|
|
1727
1815
|
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
header
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1816
|
+
try {
|
|
1817
|
+
const metadataIv = rand12();
|
|
1818
|
+
const contentIv = rand12();
|
|
1819
|
+
const keyMaterial = {
|
|
1820
|
+
salt,
|
|
1821
|
+
memoryCost: params.memoryCost,
|
|
1822
|
+
timeCost: params.timeCost,
|
|
1823
|
+
parallelism: params.parallelism
|
|
1824
|
+
};
|
|
1825
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
1826
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1827
|
+
const header = {
|
|
1828
|
+
version: OQE_FORMAT_VERSION_V2,
|
|
1829
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
1830
|
+
flags: 0,
|
|
1831
|
+
metadataLength: metadataBytes.length + AES_GCM_TAG_LEN,
|
|
1832
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1833
|
+
iv: metadataIv,
|
|
1834
|
+
contentIv
|
|
1835
|
+
};
|
|
1836
|
+
const aad = writeOQEHeader(header);
|
|
1837
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, metadataIv, aad);
|
|
1838
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, contentIv, aad);
|
|
1839
|
+
const fileData = assembleOQEFile({
|
|
1840
|
+
header,
|
|
1841
|
+
keyMaterial: keyMaterialBytes,
|
|
1842
|
+
encryptedMetadata,
|
|
1843
|
+
encryptedContent
|
|
1844
|
+
});
|
|
1845
|
+
return {
|
|
1846
|
+
data: fileData,
|
|
1847
|
+
filename: addOQEExtension(metadata.filename),
|
|
1848
|
+
metadata,
|
|
1849
|
+
mode: "password"
|
|
1850
|
+
};
|
|
1851
|
+
} finally {
|
|
1852
|
+
contentKey.fill(0);
|
|
1853
|
+
}
|
|
1759
1854
|
}
|
|
1760
1855
|
async function encryptFile(input, options) {
|
|
1761
1856
|
const plaintext = await toUint8Array(input.data);
|
|
@@ -1783,17 +1878,56 @@ async function encryptFileWithPassword(input, password) {
|
|
|
1783
1878
|
password
|
|
1784
1879
|
});
|
|
1785
1880
|
}
|
|
1786
|
-
function
|
|
1881
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
1787
1882
|
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1788
1883
|
}
|
|
1884
|
+
function headerAad(header) {
|
|
1885
|
+
return writeOQEHeader(header);
|
|
1886
|
+
}
|
|
1789
1887
|
async function decryptHybrid(encryptedData, options) {
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1888
|
+
const parsed = parseOQEFile(encryptedData);
|
|
1889
|
+
const { header } = parsed;
|
|
1890
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_MLKEM1024_AES256GCM) {
|
|
1891
|
+
return decryptHybridV2(parsed, options);
|
|
1892
|
+
}
|
|
1893
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
1894
|
+
return decryptHybridV1Legacy(parsed, options);
|
|
1796
1895
|
}
|
|
1896
|
+
throw new OQEError(
|
|
1897
|
+
"UNSUPPORTED_ALGORITHM",
|
|
1898
|
+
"This file was not encrypted with hybrid mode. Use password decryption."
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
async function decryptHybridV2(parsed, options) {
|
|
1902
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
1903
|
+
if (header.version !== OQE_FORMAT_VERSION_V2 || !header.contentIv) {
|
|
1904
|
+
throw new OQEError("INVALID_HEADER", "v2 hybrid file missing content IV");
|
|
1905
|
+
}
|
|
1906
|
+
const km = parseHybridKeyMaterialV2(keyMaterial);
|
|
1907
|
+
const x25519EpkHex = toHex(km.x25519EphemeralPk);
|
|
1908
|
+
const kyberKemCtB64 = b64(km.kyberCiphertext);
|
|
1909
|
+
const kyberShared = await kyberDecapsulate(kyberKemCtB64, options.recipientSecretKeys.kyberSecB64);
|
|
1910
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1911
|
+
const x25519Shared = nacl4.scalarMult(sk, km.x25519EphemeralPk);
|
|
1912
|
+
const kek = combinedFileKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
1913
|
+
kyberShared.fill(0);
|
|
1914
|
+
x25519Shared.fill(0);
|
|
1915
|
+
const contentKey = nacl4.secretbox.open(km.ckWrapped, km.ckWrapNonce, kek);
|
|
1916
|
+
kek.fill(0);
|
|
1917
|
+
if (!contentKey) {
|
|
1918
|
+
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
1919
|
+
}
|
|
1920
|
+
const aad = headerAad(header);
|
|
1921
|
+
try {
|
|
1922
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, aad, "metadata");
|
|
1923
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.contentIv, aad);
|
|
1924
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
1925
|
+
} finally {
|
|
1926
|
+
contentKey.fill(0);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function decryptHybridV1Legacy(parsed, options) {
|
|
1930
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parsed;
|
|
1797
1931
|
const km = parseHybridKeyMaterial(keyMaterial);
|
|
1798
1932
|
let contentKey = null;
|
|
1799
1933
|
if (await isKyberAvailable()) {
|
|
@@ -1802,54 +1936,47 @@ async function decryptHybrid(encryptedData, options) {
|
|
|
1802
1936
|
toB64(km.kyberCiphertext),
|
|
1803
1937
|
options.recipientSecretKeys.kyberSecB64
|
|
1804
1938
|
);
|
|
1805
|
-
const kyberKek =
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
contentKey = unwrapped;
|
|
1809
|
-
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
1810
|
-
}
|
|
1811
|
-
} catch (e) {
|
|
1812
|
-
console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
|
|
1939
|
+
const kyberKek = hkdfFlex2(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
|
|
1940
|
+
contentKey = nacl4.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
1941
|
+
} catch {
|
|
1813
1942
|
}
|
|
1814
1943
|
}
|
|
1815
1944
|
if (!contentKey) {
|
|
1816
1945
|
try {
|
|
1817
|
-
const ephPk = km.x25519EphemeralPk;
|
|
1818
1946
|
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1819
|
-
const x25519Shared = nacl4.scalarMult(sk,
|
|
1820
|
-
const x25519Kek =
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
contentKey = unwrapped;
|
|
1824
|
-
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
1825
|
-
}
|
|
1826
|
-
} catch (e) {
|
|
1827
|
-
console.warn("[OQE] X25519 decryption failed:", e);
|
|
1947
|
+
const x25519Shared = nacl4.scalarMult(sk, km.x25519EphemeralPk);
|
|
1948
|
+
const x25519Kek = hkdfFlex2(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
1949
|
+
contentKey = nacl4.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
1950
|
+
} catch {
|
|
1828
1951
|
}
|
|
1829
1952
|
}
|
|
1830
1953
|
if (!contentKey) {
|
|
1831
1954
|
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
1832
1955
|
}
|
|
1833
|
-
let metadata;
|
|
1834
1956
|
try {
|
|
1835
|
-
const
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1957
|
+
const metadata = await decryptSection(encryptedMetadata, contentKey, header.iv, void 0, "metadata");
|
|
1958
|
+
const plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
1959
|
+
return buildResult(plaintext, metadata, "hybrid");
|
|
1960
|
+
} finally {
|
|
1961
|
+
contentKey.fill(0);
|
|
1839
1962
|
}
|
|
1840
|
-
|
|
1963
|
+
}
|
|
1964
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
1841
1965
|
try {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1966
|
+
const bytes = await aesDecrypt(ciphertext, key, iv, aad);
|
|
1967
|
+
return parseMetadata(bytes);
|
|
1968
|
+
} catch {
|
|
1969
|
+
throw new OQEError("DECRYPTION_FAILED", `Failed to decrypt file ${what}`);
|
|
1845
1970
|
}
|
|
1971
|
+
}
|
|
1972
|
+
function buildResult(plaintext, metadata, mode) {
|
|
1846
1973
|
return {
|
|
1847
1974
|
data: plaintext,
|
|
1848
1975
|
filename: metadata.filename,
|
|
1849
1976
|
mimeType: metadata.mimeType,
|
|
1850
1977
|
originalSize: metadata.originalSize,
|
|
1851
1978
|
metadata,
|
|
1852
|
-
mode
|
|
1979
|
+
mode
|
|
1853
1980
|
};
|
|
1854
1981
|
}
|
|
1855
1982
|
async function decryptPassword(encryptedData, options) {
|
|
@@ -1871,27 +1998,27 @@ async function decryptPassword(encryptedData, options) {
|
|
|
1871
1998
|
hashLength: 32,
|
|
1872
1999
|
saltLength: km.salt.length
|
|
1873
2000
|
});
|
|
1874
|
-
|
|
2001
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
2002
|
+
const aad = isV2 ? headerAad(header) : void 0;
|
|
2003
|
+
const contentIv = isV2 && header.contentIv ? header.contentIv : header.iv;
|
|
1875
2004
|
try {
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
plaintext
|
|
1884
|
-
|
|
1885
|
-
|
|
2005
|
+
let metadata;
|
|
2006
|
+
try {
|
|
2007
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv, aad);
|
|
2008
|
+
metadata = parseMetadata(metadataBytes);
|
|
2009
|
+
} catch {
|
|
2010
|
+
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
2011
|
+
}
|
|
2012
|
+
let plaintext;
|
|
2013
|
+
try {
|
|
2014
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, contentIv, aad);
|
|
2015
|
+
} catch {
|
|
2016
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
2017
|
+
}
|
|
2018
|
+
return buildResult(plaintext, metadata, "password");
|
|
2019
|
+
} finally {
|
|
2020
|
+
contentKey.fill(0);
|
|
1886
2021
|
}
|
|
1887
|
-
return {
|
|
1888
|
-
data: plaintext,
|
|
1889
|
-
filename: metadata.filename,
|
|
1890
|
-
mimeType: metadata.mimeType,
|
|
1891
|
-
originalSize: metadata.originalSize,
|
|
1892
|
-
metadata,
|
|
1893
|
-
mode: "password"
|
|
1894
|
-
};
|
|
1895
2022
|
}
|
|
1896
2023
|
async function decryptFile(encryptedData, options) {
|
|
1897
2024
|
const data = await toUint8Array(encryptedData);
|
|
@@ -1988,4 +2115,4 @@ function createTunnelSession(keys) {
|
|
|
1988
2115
|
};
|
|
1989
2116
|
}
|
|
1990
2117
|
|
|
1991
|
-
export { BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, ENVELOPE_AEAD, ENVELOPE_SUITE, ENVELOPE_VERSION, KDF_CONFIG_ARGON2ID, KDF_CONFIG_PBKDF2, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, SecureBuffer, TUNNEL_KEY_SIZE, TUNNEL_NONCE_SIZE, TUNNEL_VERSION, VAULT_ALGORITHM, VAULT_ENCRYPTED_VERSION, VAULT_ENCRYPTED_VERSION_V2, VAULT_KDF, VAULT_KDF_V2, VAULT_VERSION, XCHACHA20_NONCE_SIZE, addIdentity, assertLen, b64, benchmarkKDF, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, computeIntegrityHash, computeKeyFingerprint, constantTimeEqual, createChaCha20Poly1305, createEmptyVault, createIdentity, createSession, createTunnelSession, createXChaCha20Poly1305, decryptFile, decryptFileWithPassword, decryptVault, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, encryptFile, encryptFileWithPassword, encryptVault, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateSalt, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getRecommendedConfig, getSecretKeys, getVaultKdfInfo, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, isSessionTimedOut, isV2Vault, kdfDeriveKey, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, lockSecureSession, migrateEncryptedVault, needsMigration, rand12, rand24, rand32, randN, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, unlockSecureSession, validateEncryptedVault, validateEnvelope, validateVault, withSecureData, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt, zeroAll, zeroMemory };
|
|
2118
|
+
export { BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, ENVELOPE_AEAD, ENVELOPE_SUITE, ENVELOPE_SUITE_V2, ENVELOPE_VERSION, ENVELOPE_VERSION_V2, KDF_CONFIG_ARGON2ID, KDF_CONFIG_PBKDF2, KYBER_CIPHERTEXT_SIZE, KYBER_PUBLIC_KEY_SIZE, KYBER_SECRET_KEY_SIZE, KYBER_SEED_SIZE, KYBER_SHARED_SECRET_SIZE, KYBER_SUITE, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, SecureBuffer, TUNNEL_KEY_SIZE, TUNNEL_NONCE_SIZE, TUNNEL_VERSION, VAULT_ALGORITHM, VAULT_ENCRYPTED_VERSION, VAULT_ENCRYPTED_VERSION_V2, VAULT_KDF, VAULT_KDF_V2, VAULT_VERSION, XCHACHA20_NONCE_SIZE, addIdentity, assertLen, b64, benchmarkKDF, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, computeIntegrityHash, computeKeyFingerprint, constantTimeEqual, createChaCha20Poly1305, createEmptyVault, createIdentity, createSession, createTunnelSession, createXChaCha20Poly1305, decryptFile, decryptFileWithPassword, decryptVault, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, encryptFile, encryptFileWithPassword, encryptVault, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateSalt, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getRecommendedConfig, getSecretKeys, getVaultKdfInfo, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, isSessionTimedOut, isV2Vault, kdfDeriveKey, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, lockSecureSession, migrateEncryptedVault, needsMigration, rand12, rand24, rand32, randN, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, unlockSecureSession, validateEncryptedVault, validateEnvelope, validateVault, withSecureData, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt, zeroAll, zeroMemory };
|