@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.js
CHANGED
|
@@ -149,6 +149,11 @@ function deriveKeyFromShared(shared, salt, info) {
|
|
|
149
149
|
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
150
150
|
}
|
|
151
151
|
var KYBER_SUITE = "ML-KEM-1024-FIPS203";
|
|
152
|
+
var KYBER_PUBLIC_KEY_SIZE = 1568;
|
|
153
|
+
var KYBER_SECRET_KEY_SIZE = 3168;
|
|
154
|
+
var KYBER_CIPHERTEXT_SIZE = 1568;
|
|
155
|
+
var KYBER_SHARED_SECRET_SIZE = 32;
|
|
156
|
+
var KYBER_SEED_SIZE = 64;
|
|
152
157
|
async function isKyberAvailable() {
|
|
153
158
|
return true;
|
|
154
159
|
}
|
|
@@ -196,17 +201,20 @@ function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
|
196
201
|
return nacl4.secretbox.open(wrapped, nonce, kek) || null;
|
|
197
202
|
}
|
|
198
203
|
var ENVELOPE_VERSION = OMNI_VERSIONS.HYBRID_V1;
|
|
204
|
+
var ENVELOPE_VERSION_V2 = OMNI_VERSIONS.HYBRID_V2;
|
|
199
205
|
var ENVELOPE_VERSION_LEGACY = DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
|
|
200
206
|
var VAULT_VERSION = "omnituum.vault.v1";
|
|
201
207
|
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
202
208
|
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
203
209
|
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
210
|
+
var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
|
|
204
211
|
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
205
212
|
var VAULT_KDF = "PBKDF2-SHA256";
|
|
206
213
|
var VAULT_KDF_V2 = "Argon2id";
|
|
207
214
|
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
208
215
|
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
209
216
|
ENVELOPE_VERSION,
|
|
217
|
+
ENVELOPE_VERSION_V2,
|
|
210
218
|
ENVELOPE_VERSION_LEGACY
|
|
211
219
|
];
|
|
212
220
|
var SUPPORTED_VAULT_VERSIONS = [
|
|
@@ -251,6 +259,8 @@ function isVaultVersionSupported(version) {
|
|
|
251
259
|
function isVaultEncryptedVersionSupported(version) {
|
|
252
260
|
return SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version);
|
|
253
261
|
}
|
|
262
|
+
var ENVELOPE_V1_FIELDS = ["x25519Epk", "x25519Wrap", "kyberKemCt", "kyberWrap", "contentNonce", "ciphertext", "meta"];
|
|
263
|
+
var ENVELOPE_V2_FIELDS = ["x25519Epk", "kyberKemCt", "ckWrap", "contentNonce", "ciphertext", "meta"];
|
|
254
264
|
function validateEnvelope(envelope) {
|
|
255
265
|
const errors = [];
|
|
256
266
|
if (!envelope || typeof envelope !== "object") {
|
|
@@ -262,14 +272,16 @@ function validateEnvelope(envelope) {
|
|
|
262
272
|
} else if (!isEnvelopeVersionSupported(env.v)) {
|
|
263
273
|
errors.push(`Unsupported envelope version: ${env.v}`);
|
|
264
274
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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}"`);
|
|
267
280
|
}
|
|
268
281
|
if (env.aead !== ENVELOPE_AEAD) {
|
|
269
282
|
errors.push(`Invalid aead: expected "${ENVELOPE_AEAD}", got "${env.aead}"`);
|
|
270
283
|
}
|
|
271
|
-
const
|
|
272
|
-
for (const field of required) {
|
|
284
|
+
for (const field of requiredFields) {
|
|
273
285
|
if (!(field in env)) {
|
|
274
286
|
errors.push(`Missing required field: ${field}`);
|
|
275
287
|
}
|
|
@@ -364,7 +376,6 @@ async function generateHybridIdentity(name) {
|
|
|
364
376
|
const x25519 = generateX25519Keypair();
|
|
365
377
|
const kyber = await generateKyberKeypair();
|
|
366
378
|
if (!kyber) {
|
|
367
|
-
console.error("Kyber key generation failed - library not available");
|
|
368
379
|
return null;
|
|
369
380
|
}
|
|
370
381
|
return {
|
|
@@ -390,49 +401,74 @@ function getSecretKeys(identity) {
|
|
|
390
401
|
kyberSecB64: identity.kyberSecB64
|
|
391
402
|
};
|
|
392
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
|
+
}
|
|
393
414
|
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
394
415
|
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
395
416
|
const CK = rand32();
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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;
|
|
430
469
|
}
|
|
431
|
-
async function
|
|
432
|
-
const version = envelope.v;
|
|
433
|
-
assertEnvelopeVersion(version);
|
|
470
|
+
async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
|
|
434
471
|
let CK = null;
|
|
435
|
-
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
436
472
|
try {
|
|
437
473
|
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
438
474
|
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
@@ -441,11 +477,7 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
441
477
|
ub64(envelope.kyberWrap.nonce),
|
|
442
478
|
kyberKek
|
|
443
479
|
);
|
|
444
|
-
|
|
445
|
-
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
446
|
-
}
|
|
447
|
-
} catch (e) {
|
|
448
|
-
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
480
|
+
} catch {
|
|
449
481
|
}
|
|
450
482
|
if (!CK) {
|
|
451
483
|
try {
|
|
@@ -458,21 +490,28 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
458
490
|
ub64(envelope.x25519Wrap.nonce),
|
|
459
491
|
x25519Kek
|
|
460
492
|
);
|
|
461
|
-
|
|
462
|
-
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
463
|
-
}
|
|
464
|
-
} catch (e) {
|
|
465
|
-
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
493
|
+
} catch {
|
|
466
494
|
}
|
|
467
495
|
}
|
|
468
496
|
if (!CK) {
|
|
469
497
|
throw new Error("Could not unwrap content key with either algorithm");
|
|
470
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
|
+
);
|
|
471
509
|
const pt = nacl4.secretbox.open(
|
|
472
510
|
ub64(envelope.ciphertext),
|
|
473
511
|
ub64(envelope.contentNonce),
|
|
474
512
|
CK
|
|
475
513
|
);
|
|
514
|
+
CK.fill(0);
|
|
476
515
|
if (!pt) {
|
|
477
516
|
throw new Error("Content authentication failed");
|
|
478
517
|
}
|
|
@@ -491,8 +530,7 @@ async function loadDilithium() {
|
|
|
491
530
|
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
|
|
492
531
|
dilithiumModule = ml_dsa65;
|
|
493
532
|
return dilithiumModule;
|
|
494
|
-
} catch
|
|
495
|
-
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
533
|
+
} catch {
|
|
496
534
|
return null;
|
|
497
535
|
}
|
|
498
536
|
}
|
|
@@ -510,8 +548,7 @@ async function generateDilithiumKeypair() {
|
|
|
510
548
|
publicB64: toB64(kp.publicKey),
|
|
511
549
|
secretB64: toB64(kp.secretKey)
|
|
512
550
|
};
|
|
513
|
-
} catch
|
|
514
|
-
console.warn("[Dilithium] Key generation failed:", e);
|
|
551
|
+
} catch {
|
|
515
552
|
return null;
|
|
516
553
|
}
|
|
517
554
|
}
|
|
@@ -850,17 +887,11 @@ function computeIntegrityHash(identities) {
|
|
|
850
887
|
createdAt: i.createdAt,
|
|
851
888
|
rotationCount: i.rotationCount
|
|
852
889
|
}));
|
|
853
|
-
const serialized = JSON.stringify(canonical
|
|
890
|
+
const serialized = JSON.stringify(canonical);
|
|
854
891
|
return computeStringHash(serialized);
|
|
855
892
|
}
|
|
856
893
|
function computeStringHash(str) {
|
|
857
|
-
|
|
858
|
-
for (let i = 0; i < str.length; i++) {
|
|
859
|
-
const char = str.charCodeAt(i);
|
|
860
|
-
hash = (hash << 5) - hash + char;
|
|
861
|
-
hash = hash & hash;
|
|
862
|
-
}
|
|
863
|
-
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
894
|
+
return toHex(sha256(textEncoder.encode(str)));
|
|
864
895
|
}
|
|
865
896
|
async function computeHashAsync(data) {
|
|
866
897
|
const encoder = new TextEncoder();
|
|
@@ -1111,7 +1142,6 @@ async function createIdentity(name) {
|
|
|
1111
1142
|
const x25519 = generateX25519Keypair();
|
|
1112
1143
|
const kyber = await generateKyberKeypair();
|
|
1113
1144
|
if (!kyber) {
|
|
1114
|
-
console.error("Kyber key generation failed");
|
|
1115
1145
|
return null;
|
|
1116
1146
|
}
|
|
1117
1147
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1334,12 +1364,22 @@ async function migrateEncryptedVault(options) {
|
|
|
1334
1364
|
|
|
1335
1365
|
// src/fs/types.ts
|
|
1336
1366
|
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
1337
|
-
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];
|
|
1338
1370
|
var ALGORITHM_SUITES = {
|
|
1339
|
-
/**
|
|
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
|
+
*/
|
|
1340
1375
|
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
1341
1376
|
/** Password: Argon2id + AES-256-GCM */
|
|
1342
|
-
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
|
|
1343
1383
|
};
|
|
1344
1384
|
var DEFAULT_ARGON2ID_PARAMS = {
|
|
1345
1385
|
memoryCost: 65536,
|
|
@@ -1349,7 +1389,8 @@ var DEFAULT_ARGON2ID_PARAMS = {
|
|
|
1349
1389
|
hashLength: 32,
|
|
1350
1390
|
saltLength: 32
|
|
1351
1391
|
};
|
|
1352
|
-
var
|
|
1392
|
+
var OQE_HEADER_SIZE_V1 = 30;
|
|
1393
|
+
var OQE_HEADER_SIZE_V2 = 42;
|
|
1353
1394
|
var OQEError = class extends Error {
|
|
1354
1395
|
constructor(code, message) {
|
|
1355
1396
|
super(message);
|
|
@@ -1419,6 +1460,7 @@ function toArrayBuffer(arr) {
|
|
|
1419
1460
|
}
|
|
1420
1461
|
var AES_KEY_SIZE = 32;
|
|
1421
1462
|
var AES_GCM_IV_SIZE = 12;
|
|
1463
|
+
var AES_GCM_TAG_SIZE = 16;
|
|
1422
1464
|
async function importAesKey(keyBytes) {
|
|
1423
1465
|
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
1424
1466
|
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
@@ -1442,7 +1484,7 @@ async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
|
1442
1484
|
{
|
|
1443
1485
|
name: "AES-GCM",
|
|
1444
1486
|
iv: toArrayBuffer(ivBytes),
|
|
1445
|
-
additionalData: void 0,
|
|
1487
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
1446
1488
|
tagLength: 128
|
|
1447
1489
|
// 16 bytes
|
|
1448
1490
|
},
|
|
@@ -1478,6 +1520,9 @@ async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
|
1478
1520
|
}
|
|
1479
1521
|
|
|
1480
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
|
+
}
|
|
1481
1526
|
function writeUint32BE(value) {
|
|
1482
1527
|
const buffer = new ArrayBuffer(4);
|
|
1483
1528
|
new DataView(buffer).setUint32(0, value, false);
|
|
@@ -1495,7 +1540,8 @@ function readUint16BE(data, offset) {
|
|
|
1495
1540
|
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
1496
1541
|
}
|
|
1497
1542
|
function writeOQEHeader(header) {
|
|
1498
|
-
const
|
|
1543
|
+
const isV2 = header.version === OQE_FORMAT_VERSION_V2;
|
|
1544
|
+
const buffer = new Uint8Array(oqeHeaderSize(header.version));
|
|
1499
1545
|
let offset = 0;
|
|
1500
1546
|
buffer.set(OQE_MAGIC, offset);
|
|
1501
1547
|
offset += 4;
|
|
@@ -1508,11 +1554,18 @@ function writeOQEHeader(header) {
|
|
|
1508
1554
|
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
1509
1555
|
offset += 4;
|
|
1510
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
|
+
}
|
|
1511
1564
|
return buffer;
|
|
1512
1565
|
}
|
|
1513
1566
|
function parseOQEHeader(data) {
|
|
1514
|
-
if (data.length <
|
|
1515
|
-
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}`);
|
|
1516
1569
|
}
|
|
1517
1570
|
let offset = 0;
|
|
1518
1571
|
const magic = data.slice(0, 4);
|
|
@@ -1521,14 +1574,19 @@ function parseOQEHeader(data) {
|
|
|
1521
1574
|
}
|
|
1522
1575
|
offset += 4;
|
|
1523
1576
|
const version = data[offset++];
|
|
1524
|
-
if (version
|
|
1577
|
+
if (!SUPPORTED_OQE_VERSIONS.includes(version)) {
|
|
1525
1578
|
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
1526
1579
|
}
|
|
1527
1580
|
const algorithmSuiteRaw = data[offset++];
|
|
1528
|
-
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) {
|
|
1529
1582
|
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
1530
1583
|
}
|
|
1531
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
|
+
}
|
|
1532
1590
|
const flags = readUint32BE(data, offset);
|
|
1533
1591
|
offset += 4;
|
|
1534
1592
|
const metadataLength = readUint32BE(data, offset);
|
|
@@ -1536,35 +1594,18 @@ function parseOQEHeader(data) {
|
|
|
1536
1594
|
const keyMaterialLength = readUint32BE(data, offset);
|
|
1537
1595
|
offset += 4;
|
|
1538
1596
|
const iv = data.slice(offset, offset + 12);
|
|
1597
|
+
offset += 12;
|
|
1598
|
+
const contentIv = isV2 ? data.slice(offset, offset + 12) : void 0;
|
|
1539
1599
|
return {
|
|
1540
1600
|
version,
|
|
1541
1601
|
algorithmSuite,
|
|
1542
1602
|
flags,
|
|
1543
1603
|
metadataLength,
|
|
1544
1604
|
keyMaterialLength,
|
|
1545
|
-
iv
|
|
1605
|
+
iv,
|
|
1606
|
+
contentIv
|
|
1546
1607
|
};
|
|
1547
1608
|
}
|
|
1548
|
-
function serializeHybridKeyMaterial(km) {
|
|
1549
|
-
const parts = [];
|
|
1550
|
-
parts.push(km.x25519EphemeralPk);
|
|
1551
|
-
parts.push(km.x25519Nonce);
|
|
1552
|
-
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
1553
|
-
parts.push(km.x25519WrappedKey);
|
|
1554
|
-
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1555
|
-
parts.push(km.kyberCiphertext);
|
|
1556
|
-
parts.push(km.kyberNonce);
|
|
1557
|
-
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
1558
|
-
parts.push(km.kyberWrappedKey);
|
|
1559
|
-
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1560
|
-
const result = new Uint8Array(totalLength);
|
|
1561
|
-
let offset = 0;
|
|
1562
|
-
for (const part of parts) {
|
|
1563
|
-
result.set(part, offset);
|
|
1564
|
-
offset += part.length;
|
|
1565
|
-
}
|
|
1566
|
-
return result;
|
|
1567
|
-
}
|
|
1568
1609
|
function parseHybridKeyMaterial(data) {
|
|
1569
1610
|
let offset = 0;
|
|
1570
1611
|
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
@@ -1593,6 +1634,38 @@ function parseHybridKeyMaterial(data) {
|
|
|
1593
1634
|
kyberWrappedKey
|
|
1594
1635
|
};
|
|
1595
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
|
+
}
|
|
1596
1669
|
function serializePasswordKeyMaterial(km) {
|
|
1597
1670
|
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
1598
1671
|
result.set(km.salt, 0);
|
|
@@ -1634,7 +1707,7 @@ function assembleOQEFile(components) {
|
|
|
1634
1707
|
}
|
|
1635
1708
|
function parseOQEFile(data) {
|
|
1636
1709
|
const header = parseOQEHeader(data);
|
|
1637
|
-
let offset =
|
|
1710
|
+
let offset = oqeHeaderSize(header.version);
|
|
1638
1711
|
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
1639
1712
|
offset += header.keyMaterialLength;
|
|
1640
1713
|
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
@@ -1654,61 +1727,81 @@ function addOQEExtension(filename) {
|
|
|
1654
1727
|
}
|
|
1655
1728
|
return `${filename}${OQE_EXTENSION}`;
|
|
1656
1729
|
}
|
|
1657
|
-
|
|
1658
|
-
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1659
|
-
}
|
|
1730
|
+
var AES_GCM_TAG_LEN = AES_GCM_TAG_SIZE;
|
|
1660
1731
|
function computeIdentityHash(publicKeyHex) {
|
|
1661
1732
|
const hash = sha256(fromHex(publicKeyHex));
|
|
1662
1733
|
return toHex(hash).slice(0, 16);
|
|
1663
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
|
+
}
|
|
1664
1749
|
async function encryptHybrid(plaintext, metadata, options) {
|
|
1665
1750
|
if (!await isKyberAvailable()) {
|
|
1666
1751
|
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
1667
1752
|
}
|
|
1668
1753
|
const contentKey = rand32();
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
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
|
-
encryptedMetadata,
|
|
1704
|
-
encryptedContent
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
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
|
+
}
|
|
1712
1805
|
}
|
|
1713
1806
|
async function encryptPassword(plaintext, metadata, options) {
|
|
1714
1807
|
if (!await isArgon2Available()) {
|
|
@@ -1720,37 +1813,44 @@ async function encryptPassword(plaintext, metadata, options) {
|
|
|
1720
1813
|
};
|
|
1721
1814
|
const salt = generateArgon2Salt(params.saltLength);
|
|
1722
1815
|
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
header
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
+
}
|
|
1754
1854
|
}
|
|
1755
1855
|
async function encryptFile(input, options) {
|
|
1756
1856
|
const plaintext = await toUint8Array(input.data);
|
|
@@ -1778,17 +1878,56 @@ async function encryptFileWithPassword(input, password) {
|
|
|
1778
1878
|
password
|
|
1779
1879
|
});
|
|
1780
1880
|
}
|
|
1781
|
-
function
|
|
1881
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
1782
1882
|
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1783
1883
|
}
|
|
1884
|
+
function headerAad(header) {
|
|
1885
|
+
return writeOQEHeader(header);
|
|
1886
|
+
}
|
|
1784
1887
|
async function decryptHybrid(encryptedData, options) {
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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);
|
|
1791
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;
|
|
1792
1931
|
const km = parseHybridKeyMaterial(keyMaterial);
|
|
1793
1932
|
let contentKey = null;
|
|
1794
1933
|
if (await isKyberAvailable()) {
|
|
@@ -1797,54 +1936,47 @@ async function decryptHybrid(encryptedData, options) {
|
|
|
1797
1936
|
toB64(km.kyberCiphertext),
|
|
1798
1937
|
options.recipientSecretKeys.kyberSecB64
|
|
1799
1938
|
);
|
|
1800
|
-
const kyberKek =
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
contentKey = unwrapped;
|
|
1804
|
-
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
1805
|
-
}
|
|
1806
|
-
} catch (e) {
|
|
1807
|
-
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 {
|
|
1808
1942
|
}
|
|
1809
1943
|
}
|
|
1810
1944
|
if (!contentKey) {
|
|
1811
1945
|
try {
|
|
1812
|
-
const ephPk = km.x25519EphemeralPk;
|
|
1813
1946
|
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1814
|
-
const x25519Shared = nacl4.scalarMult(sk,
|
|
1815
|
-
const x25519Kek =
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
contentKey = unwrapped;
|
|
1819
|
-
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
1820
|
-
}
|
|
1821
|
-
} catch (e) {
|
|
1822
|
-
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 {
|
|
1823
1951
|
}
|
|
1824
1952
|
}
|
|
1825
1953
|
if (!contentKey) {
|
|
1826
1954
|
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
1827
1955
|
}
|
|
1828
|
-
let metadata;
|
|
1829
1956
|
try {
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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);
|
|
1834
1962
|
}
|
|
1835
|
-
|
|
1963
|
+
}
|
|
1964
|
+
async function decryptSection(ciphertext, key, iv, aad, what) {
|
|
1836
1965
|
try {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
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}`);
|
|
1840
1970
|
}
|
|
1971
|
+
}
|
|
1972
|
+
function buildResult(plaintext, metadata, mode) {
|
|
1841
1973
|
return {
|
|
1842
1974
|
data: plaintext,
|
|
1843
1975
|
filename: metadata.filename,
|
|
1844
1976
|
mimeType: metadata.mimeType,
|
|
1845
1977
|
originalSize: metadata.originalSize,
|
|
1846
1978
|
metadata,
|
|
1847
|
-
mode
|
|
1979
|
+
mode
|
|
1848
1980
|
};
|
|
1849
1981
|
}
|
|
1850
1982
|
async function decryptPassword(encryptedData, options) {
|
|
@@ -1866,27 +1998,27 @@ async function decryptPassword(encryptedData, options) {
|
|
|
1866
1998
|
hashLength: 32,
|
|
1867
1999
|
saltLength: km.salt.length
|
|
1868
2000
|
});
|
|
1869
|
-
|
|
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;
|
|
1870
2004
|
try {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
plaintext
|
|
1879
|
-
|
|
1880
|
-
|
|
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);
|
|
1881
2021
|
}
|
|
1882
|
-
return {
|
|
1883
|
-
data: plaintext,
|
|
1884
|
-
filename: metadata.filename,
|
|
1885
|
-
mimeType: metadata.mimeType,
|
|
1886
|
-
originalSize: metadata.originalSize,
|
|
1887
|
-
metadata,
|
|
1888
|
-
mode: "password"
|
|
1889
|
-
};
|
|
1890
2022
|
}
|
|
1891
2023
|
async function decryptFile(encryptedData, options) {
|
|
1892
2024
|
const data = await toUint8Array(encryptedData);
|
|
@@ -1983,4 +2115,4 @@ function createTunnelSession(keys) {
|
|
|
1983
2115
|
};
|
|
1984
2116
|
}
|
|
1985
2117
|
|
|
1986
|
-
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_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 };
|