@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/index.cjs
CHANGED
|
@@ -155,6 +155,11 @@ function deriveKeyFromShared(shared, salt, info) {
|
|
|
155
155
|
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
156
156
|
}
|
|
157
157
|
var KYBER_SUITE = "ML-KEM-1024-FIPS203";
|
|
158
|
+
var KYBER_PUBLIC_KEY_SIZE = 1568;
|
|
159
|
+
var KYBER_SECRET_KEY_SIZE = 3168;
|
|
160
|
+
var KYBER_CIPHERTEXT_SIZE = 1568;
|
|
161
|
+
var KYBER_SHARED_SECRET_SIZE = 32;
|
|
162
|
+
var KYBER_SEED_SIZE = 64;
|
|
158
163
|
async function isKyberAvailable() {
|
|
159
164
|
return true;
|
|
160
165
|
}
|
|
@@ -202,17 +207,20 @@ function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
|
202
207
|
return nacl4__default.default.secretbox.open(wrapped, nonce, kek) || null;
|
|
203
208
|
}
|
|
204
209
|
var ENVELOPE_VERSION = envelopeRegistry.OMNI_VERSIONS.HYBRID_V1;
|
|
210
|
+
var ENVELOPE_VERSION_V2 = envelopeRegistry.OMNI_VERSIONS.HYBRID_V2;
|
|
205
211
|
var ENVELOPE_VERSION_LEGACY = envelopeRegistry.DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
|
|
206
212
|
var VAULT_VERSION = "omnituum.vault.v1";
|
|
207
213
|
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
208
214
|
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
209
215
|
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
216
|
+
var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
|
|
210
217
|
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
211
218
|
var VAULT_KDF = "PBKDF2-SHA256";
|
|
212
219
|
var VAULT_KDF_V2 = "Argon2id";
|
|
213
220
|
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
214
221
|
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
215
222
|
ENVELOPE_VERSION,
|
|
223
|
+
ENVELOPE_VERSION_V2,
|
|
216
224
|
ENVELOPE_VERSION_LEGACY
|
|
217
225
|
];
|
|
218
226
|
var SUPPORTED_VAULT_VERSIONS = [
|
|
@@ -257,6 +265,8 @@ function isVaultVersionSupported(version) {
|
|
|
257
265
|
function isVaultEncryptedVersionSupported(version) {
|
|
258
266
|
return SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version);
|
|
259
267
|
}
|
|
268
|
+
var ENVELOPE_V1_FIELDS = ["x25519Epk", "x25519Wrap", "kyberKemCt", "kyberWrap", "contentNonce", "ciphertext", "meta"];
|
|
269
|
+
var ENVELOPE_V2_FIELDS = ["x25519Epk", "kyberKemCt", "ckWrap", "contentNonce", "ciphertext", "meta"];
|
|
260
270
|
function validateEnvelope(envelope) {
|
|
261
271
|
const errors = [];
|
|
262
272
|
if (!envelope || typeof envelope !== "object") {
|
|
@@ -268,14 +278,16 @@ function validateEnvelope(envelope) {
|
|
|
268
278
|
} else if (!isEnvelopeVersionSupported(env.v)) {
|
|
269
279
|
errors.push(`Unsupported envelope version: ${env.v}`);
|
|
270
280
|
}
|
|
271
|
-
|
|
272
|
-
|
|
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}"`);
|
|
273
286
|
}
|
|
274
287
|
if (env.aead !== ENVELOPE_AEAD) {
|
|
275
288
|
errors.push(`Invalid aead: expected "${ENVELOPE_AEAD}", got "${env.aead}"`);
|
|
276
289
|
}
|
|
277
|
-
const
|
|
278
|
-
for (const field of required) {
|
|
290
|
+
for (const field of requiredFields) {
|
|
279
291
|
if (!(field in env)) {
|
|
280
292
|
errors.push(`Missing required field: ${field}`);
|
|
281
293
|
}
|
|
@@ -370,7 +382,6 @@ async function generateHybridIdentity(name) {
|
|
|
370
382
|
const x25519 = generateX25519Keypair();
|
|
371
383
|
const kyber = await generateKyberKeypair();
|
|
372
384
|
if (!kyber) {
|
|
373
|
-
console.error("Kyber key generation failed - library not available");
|
|
374
385
|
return null;
|
|
375
386
|
}
|
|
376
387
|
return {
|
|
@@ -396,49 +407,74 @@ function getSecretKeys(identity) {
|
|
|
396
407
|
kyberSecB64: identity.kyberSecB64
|
|
397
408
|
};
|
|
398
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
|
+
}
|
|
399
420
|
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
400
421
|
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
401
422
|
const CK = rand32();
|
|
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
|
-
|
|
435
|
-
|
|
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;
|
|
436
475
|
}
|
|
437
|
-
async function
|
|
438
|
-
const version = envelope.v;
|
|
439
|
-
assertEnvelopeVersion(version);
|
|
476
|
+
async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
|
|
440
477
|
let CK = null;
|
|
441
|
-
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
442
478
|
try {
|
|
443
479
|
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
444
480
|
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
@@ -447,11 +483,7 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
447
483
|
ub64(envelope.kyberWrap.nonce),
|
|
448
484
|
kyberKek
|
|
449
485
|
);
|
|
450
|
-
|
|
451
|
-
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
452
|
-
}
|
|
453
|
-
} catch (e) {
|
|
454
|
-
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
486
|
+
} catch {
|
|
455
487
|
}
|
|
456
488
|
if (!CK) {
|
|
457
489
|
try {
|
|
@@ -464,21 +496,28 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
464
496
|
ub64(envelope.x25519Wrap.nonce),
|
|
465
497
|
x25519Kek
|
|
466
498
|
);
|
|
467
|
-
|
|
468
|
-
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
469
|
-
}
|
|
470
|
-
} catch (e) {
|
|
471
|
-
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
499
|
+
} catch {
|
|
472
500
|
}
|
|
473
501
|
}
|
|
474
502
|
if (!CK) {
|
|
475
503
|
throw new Error("Could not unwrap content key with either algorithm");
|
|
476
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
|
+
);
|
|
477
515
|
const pt = nacl4__default.default.secretbox.open(
|
|
478
516
|
ub64(envelope.ciphertext),
|
|
479
517
|
ub64(envelope.contentNonce),
|
|
480
518
|
CK
|
|
481
519
|
);
|
|
520
|
+
CK.fill(0);
|
|
482
521
|
if (!pt) {
|
|
483
522
|
throw new Error("Content authentication failed");
|
|
484
523
|
}
|
|
@@ -497,8 +536,7 @@ async function loadDilithium() {
|
|
|
497
536
|
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
|
|
498
537
|
dilithiumModule = ml_dsa65;
|
|
499
538
|
return dilithiumModule;
|
|
500
|
-
} catch
|
|
501
|
-
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
539
|
+
} catch {
|
|
502
540
|
return null;
|
|
503
541
|
}
|
|
504
542
|
}
|
|
@@ -516,8 +554,7 @@ async function generateDilithiumKeypair() {
|
|
|
516
554
|
publicB64: toB64(kp.publicKey),
|
|
517
555
|
secretB64: toB64(kp.secretKey)
|
|
518
556
|
};
|
|
519
|
-
} catch
|
|
520
|
-
console.warn("[Dilithium] Key generation failed:", e);
|
|
557
|
+
} catch {
|
|
521
558
|
return null;
|
|
522
559
|
}
|
|
523
560
|
}
|
|
@@ -856,17 +893,11 @@ function computeIntegrityHash(identities) {
|
|
|
856
893
|
createdAt: i.createdAt,
|
|
857
894
|
rotationCount: i.rotationCount
|
|
858
895
|
}));
|
|
859
|
-
const serialized = JSON.stringify(canonical
|
|
896
|
+
const serialized = JSON.stringify(canonical);
|
|
860
897
|
return computeStringHash(serialized);
|
|
861
898
|
}
|
|
862
899
|
function computeStringHash(str) {
|
|
863
|
-
|
|
864
|
-
for (let i = 0; i < str.length; i++) {
|
|
865
|
-
const char = str.charCodeAt(i);
|
|
866
|
-
hash = (hash << 5) - hash + char;
|
|
867
|
-
hash = hash & hash;
|
|
868
|
-
}
|
|
869
|
-
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
900
|
+
return toHex(sha256(textEncoder.encode(str)));
|
|
870
901
|
}
|
|
871
902
|
async function computeHashAsync(data) {
|
|
872
903
|
const encoder = new TextEncoder();
|
|
@@ -1117,7 +1148,6 @@ async function createIdentity(name) {
|
|
|
1117
1148
|
const x25519 = generateX25519Keypair();
|
|
1118
1149
|
const kyber = await generateKyberKeypair();
|
|
1119
1150
|
if (!kyber) {
|
|
1120
|
-
console.error("Kyber key generation failed");
|
|
1121
1151
|
return null;
|
|
1122
1152
|
}
|
|
1123
1153
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1340,12 +1370,22 @@ async function migrateEncryptedVault(options) {
|
|
|
1340
1370
|
|
|
1341
1371
|
// src/fs/types.ts
|
|
1342
1372
|
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
1343
|
-
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];
|
|
1344
1376
|
var ALGORITHM_SUITES = {
|
|
1345
|
-
/**
|
|
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
|
+
*/
|
|
1346
1381
|
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
1347
1382
|
/** Password: Argon2id + AES-256-GCM */
|
|
1348
|
-
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
|
|
1349
1389
|
};
|
|
1350
1390
|
var DEFAULT_ARGON2ID_PARAMS = {
|
|
1351
1391
|
memoryCost: 65536,
|
|
@@ -1355,7 +1395,8 @@ var DEFAULT_ARGON2ID_PARAMS = {
|
|
|
1355
1395
|
hashLength: 32,
|
|
1356
1396
|
saltLength: 32
|
|
1357
1397
|
};
|
|
1358
|
-
var
|
|
1398
|
+
var OQE_HEADER_SIZE_V1 = 30;
|
|
1399
|
+
var OQE_HEADER_SIZE_V2 = 42;
|
|
1359
1400
|
var OQEError = class extends Error {
|
|
1360
1401
|
constructor(code, message) {
|
|
1361
1402
|
super(message);
|
|
@@ -1425,6 +1466,7 @@ function toArrayBuffer(arr) {
|
|
|
1425
1466
|
}
|
|
1426
1467
|
var AES_KEY_SIZE = 32;
|
|
1427
1468
|
var AES_GCM_IV_SIZE = 12;
|
|
1469
|
+
var AES_GCM_TAG_SIZE = 16;
|
|
1428
1470
|
async function importAesKey(keyBytes) {
|
|
1429
1471
|
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
1430
1472
|
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
@@ -1448,7 +1490,7 @@ async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
|
1448
1490
|
{
|
|
1449
1491
|
name: "AES-GCM",
|
|
1450
1492
|
iv: toArrayBuffer(ivBytes),
|
|
1451
|
-
additionalData: void 0,
|
|
1493
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
1452
1494
|
tagLength: 128
|
|
1453
1495
|
// 16 bytes
|
|
1454
1496
|
},
|
|
@@ -1484,6 +1526,9 @@ async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
|
1484
1526
|
}
|
|
1485
1527
|
|
|
1486
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
|
+
}
|
|
1487
1532
|
function writeUint32BE(value) {
|
|
1488
1533
|
const buffer = new ArrayBuffer(4);
|
|
1489
1534
|
new DataView(buffer).setUint32(0, value, false);
|
|
@@ -1501,7 +1546,8 @@ function readUint16BE(data, offset) {
|
|
|
1501
1546
|
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
1502
1547
|
}
|
|
1503
1548
|
function writeOQEHeader(header) {
|
|
1504
|
-
const
|
|
1549
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
1550
|
+
const buffer = new Uint8Array(oqeHeaderSize(header.version));
|
|
1505
1551
|
let offset = 0;
|
|
1506
1552
|
buffer.set(OQE_MAGIC, offset);
|
|
1507
1553
|
offset += 4;
|
|
@@ -1514,11 +1560,18 @@ function writeOQEHeader(header) {
|
|
|
1514
1560
|
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
1515
1561
|
offset += 4;
|
|
1516
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
|
+
}
|
|
1517
1570
|
return buffer;
|
|
1518
1571
|
}
|
|
1519
1572
|
function parseOQEHeader(data) {
|
|
1520
|
-
if (data.length <
|
|
1521
|
-
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}`);
|
|
1522
1575
|
}
|
|
1523
1576
|
let offset = 0;
|
|
1524
1577
|
const magic = data.slice(0, 4);
|
|
@@ -1527,14 +1580,19 @@ function parseOQEHeader(data) {
|
|
|
1527
1580
|
}
|
|
1528
1581
|
offset += 4;
|
|
1529
1582
|
const version = data[offset++];
|
|
1530
|
-
if (version
|
|
1583
|
+
if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
|
|
1531
1584
|
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
1532
1585
|
}
|
|
1533
1586
|
const algorithmSuiteRaw = data[offset++];
|
|
1534
|
-
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) {
|
|
1535
1588
|
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
1536
1589
|
}
|
|
1537
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
|
+
}
|
|
1538
1596
|
const flags = readUint32BE(data, offset);
|
|
1539
1597
|
offset += 4;
|
|
1540
1598
|
const metadataLength = readUint32BE(data, offset);
|
|
@@ -1542,35 +1600,18 @@ function parseOQEHeader(data) {
|
|
|
1542
1600
|
const keyMaterialLength = readUint32BE(data, offset);
|
|
1543
1601
|
offset += 4;
|
|
1544
1602
|
const iv = data.slice(offset, offset + 12);
|
|
1603
|
+
offset += 12;
|
|
1604
|
+
const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
|
|
1545
1605
|
return {
|
|
1546
1606
|
version,
|
|
1547
1607
|
algorithmSuite,
|
|
1548
1608
|
flags,
|
|
1549
1609
|
metadataLength,
|
|
1550
1610
|
keyMaterialLength,
|
|
1551
|
-
iv
|
|
1611
|
+
iv,
|
|
1612
|
+
contentIv
|
|
1552
1613
|
};
|
|
1553
1614
|
}
|
|
1554
|
-
function serializeHybridKeyMaterial(km) {
|
|
1555
|
-
const parts = [];
|
|
1556
|
-
parts.push(km.x25519EphemeralPk);
|
|
1557
|
-
parts.push(km.x25519Nonce);
|
|
1558
|
-
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
1559
|
-
parts.push(km.x25519WrappedKey);
|
|
1560
|
-
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1561
|
-
parts.push(km.kyberCiphertext);
|
|
1562
|
-
parts.push(km.kyberNonce);
|
|
1563
|
-
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
1564
|
-
parts.push(km.kyberWrappedKey);
|
|
1565
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1566
|
-
const result = new Uint8Array(totalLength);
|
|
1567
|
-
let offset = 0;
|
|
1568
|
-
for (const part of parts) {
|
|
1569
|
-
result.set(part, offset);
|
|
1570
|
-
offset += part.length;
|
|
1571
|
-
}
|
|
1572
|
-
return result;
|
|
1573
|
-
}
|
|
1574
1615
|
function parseHybridKeyMaterial(data) {
|
|
1575
1616
|
let offset = 0;
|
|
1576
1617
|
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
@@ -1599,6 +1640,38 @@ function parseHybridKeyMaterial(data) {
|
|
|
1599
1640
|
kyberWrappedKey
|
|
1600
1641
|
};
|
|
1601
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
|
+
}
|
|
1602
1675
|
function serializePasswordKeyMaterial(km) {
|
|
1603
1676
|
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
1604
1677
|
result.set(km.salt, 0);
|
|
@@ -1640,7 +1713,7 @@ function assembleOQEFile(components) {
|
|
|
1640
1713
|
}
|
|
1641
1714
|
function parseOQEFile(data) {
|
|
1642
1715
|
const header = parseOQEHeader(data);
|
|
1643
|
-
let offset =
|
|
1716
|
+
let offset = oqeHeaderSize(header.version);
|
|
1644
1717
|
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
1645
1718
|
offset += header.keyMaterialLength;
|
|
1646
1719
|
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
@@ -1660,61 +1733,81 @@ function addOQEExtension(filename) {
|
|
|
1660
1733
|
}
|
|
1661
1734
|
return `${filename}${OQE_EXTENSION}`;
|
|
1662
1735
|
}
|
|
1663
|
-
|
|
1664
|
-
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1665
|
-
}
|
|
1736
|
+
var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
|
|
1666
1737
|
function computeIdentityHash(publicKeyHex) {
|
|
1667
1738
|
const hash = sha256(fromHex(publicKeyHex));
|
|
1668
1739
|
return toHex(hash).slice(0, 16);
|
|
1669
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
|
+
}
|
|
1670
1755
|
async function encryptHybrid(plaintext, metadata, options) {
|
|
1671
1756
|
if (!await isKyberAvailable()) {
|
|
1672
1757
|
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
1673
1758
|
}
|
|
1674
1759
|
const contentKey = rand32();
|
|
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
|
-
|
|
1709
|
-
encryptedMetadata,
|
|
1710
|
-
encryptedContent
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
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
|
+
}
|
|
1718
1811
|
}
|
|
1719
1812
|
async function encryptPassword(plaintext, metadata, options) {
|
|
1720
1813
|
if (!await isArgon2Available()) {
|
|
@@ -1726,37 +1819,44 @@ async function encryptPassword(plaintext, metadata, options) {
|
|
|
1726
1819
|
};
|
|
1727
1820
|
const salt = generateArgon2Salt(params.saltLength);
|
|
1728
1821
|
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
header
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
+
}
|
|
1760
1860
|
}
|
|
1761
1861
|
async function encryptFile(input, options) {
|
|
1762
1862
|
const plaintext = await toUint8Array(input.data);
|
|
@@ -1784,17 +1884,56 @@ async function encryptFileWithPassword(input, password) {
|
|
|
1784
1884
|
password
|
|
1785
1885
|
});
|
|
1786
1886
|
}
|
|
1787
|
-
function
|
|
1887
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
1788
1888
|
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1789
1889
|
}
|
|
1890
|
+
function headerAad(header) {
|
|
1891
|
+
return writeOQEHeader(header);
|
|
1892
|
+
}
|
|
1790
1893
|
async function decryptHybrid(encryptedData, options) {
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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);
|
|
1797
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;
|
|
1798
1937
|
const km = parseHybridKeyMaterial(keyMaterial);
|
|
1799
1938
|
let contentKey = null;
|
|
1800
1939
|
if (await isKyberAvailable()) {
|
|
@@ -1803,54 +1942,47 @@ async function decryptHybrid(encryptedData, options) {
|
|
|
1803
1942
|
toB64(km.kyberCiphertext),
|
|
1804
1943
|
options.recipientSecretKeys.kyberSecB64
|
|
1805
1944
|
);
|
|
1806
|
-
const kyberKek =
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
contentKey = unwrapped;
|
|
1810
|
-
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
1811
|
-
}
|
|
1812
|
-
} catch (e) {
|
|
1813
|
-
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 {
|
|
1814
1948
|
}
|
|
1815
1949
|
}
|
|
1816
1950
|
if (!contentKey) {
|
|
1817
1951
|
try {
|
|
1818
|
-
const ephPk = km.x25519EphemeralPk;
|
|
1819
1952
|
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1820
|
-
const x25519Shared = nacl4__default.default.scalarMult(sk,
|
|
1821
|
-
const x25519Kek =
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
contentKey = unwrapped;
|
|
1825
|
-
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
1826
|
-
}
|
|
1827
|
-
} catch (e) {
|
|
1828
|
-
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 {
|
|
1829
1957
|
}
|
|
1830
1958
|
}
|
|
1831
1959
|
if (!contentKey) {
|
|
1832
1960
|
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
1833
1961
|
}
|
|
1834
|
-
let metadata;
|
|
1835
1962
|
try {
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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);
|
|
1840
1968
|
}
|
|
1841
|
-
|
|
1969
|
+
}
|
|
1970
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
1842
1971
|
try {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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}`);
|
|
1846
1976
|
}
|
|
1977
|
+
}
|
|
1978
|
+
function buildResult(plaintext, metadata, mode) {
|
|
1847
1979
|
return {
|
|
1848
1980
|
data: plaintext,
|
|
1849
1981
|
filename: metadata.filename,
|
|
1850
1982
|
mimeType: metadata.mimeType,
|
|
1851
1983
|
originalSize: metadata.originalSize,
|
|
1852
1984
|
metadata,
|
|
1853
|
-
mode
|
|
1985
|
+
mode
|
|
1854
1986
|
};
|
|
1855
1987
|
}
|
|
1856
1988
|
async function decryptPassword(encryptedData, options) {
|
|
@@ -1872,27 +2004,27 @@ async function decryptPassword(encryptedData, options) {
|
|
|
1872
2004
|
hashLength: 32,
|
|
1873
2005
|
saltLength: km.salt.length
|
|
1874
2006
|
});
|
|
1875
|
-
|
|
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;
|
|
1876
2010
|
try {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
plaintext
|
|
1885
|
-
|
|
1886
|
-
|
|
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);
|
|
1887
2027
|
}
|
|
1888
|
-
return {
|
|
1889
|
-
data: plaintext,
|
|
1890
|
-
filename: metadata.filename,
|
|
1891
|
-
mimeType: metadata.mimeType,
|
|
1892
|
-
originalSize: metadata.originalSize,
|
|
1893
|
-
metadata,
|
|
1894
|
-
mode: "password"
|
|
1895
|
-
};
|
|
1896
2028
|
}
|
|
1897
2029
|
async function decryptFile(encryptedData, options) {
|
|
1898
2030
|
const data = await toUint8Array(encryptedData);
|
|
@@ -1999,9 +2131,16 @@ exports.DILITHIUM_SECRET_KEY_SIZE = DILITHIUM_SECRET_KEY_SIZE;
|
|
|
1999
2131
|
exports.DILITHIUM_SIGNATURE_SIZE = DILITHIUM_SIGNATURE_SIZE;
|
|
2000
2132
|
exports.ENVELOPE_AEAD = ENVELOPE_AEAD;
|
|
2001
2133
|
exports.ENVELOPE_SUITE = ENVELOPE_SUITE;
|
|
2134
|
+
exports.ENVELOPE_SUITE_V2 = ENVELOPE_SUITE_V2;
|
|
2002
2135
|
exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
|
|
2136
|
+
exports.ENVELOPE_VERSION_V2 = ENVELOPE_VERSION_V2;
|
|
2003
2137
|
exports.KDF_CONFIG_ARGON2ID = KDF_CONFIG_ARGON2ID;
|
|
2004
2138
|
exports.KDF_CONFIG_PBKDF2 = KDF_CONFIG_PBKDF2;
|
|
2139
|
+
exports.KYBER_CIPHERTEXT_SIZE = KYBER_CIPHERTEXT_SIZE;
|
|
2140
|
+
exports.KYBER_PUBLIC_KEY_SIZE = KYBER_PUBLIC_KEY_SIZE;
|
|
2141
|
+
exports.KYBER_SECRET_KEY_SIZE = KYBER_SECRET_KEY_SIZE;
|
|
2142
|
+
exports.KYBER_SEED_SIZE = KYBER_SEED_SIZE;
|
|
2143
|
+
exports.KYBER_SHARED_SECRET_SIZE = KYBER_SHARED_SECRET_SIZE;
|
|
2005
2144
|
exports.KYBER_SUITE = KYBER_SUITE;
|
|
2006
2145
|
exports.POLY1305_TAG_SIZE = POLY1305_TAG_SIZE;
|
|
2007
2146
|
exports.SECRETBOX_KEY_SIZE = SECRETBOX_KEY_SIZE;
|