@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/crypto/index.js
CHANGED
|
@@ -195,6 +195,11 @@ function deriveKeyFromShared(shared, salt, info) {
|
|
|
195
195
|
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
196
196
|
}
|
|
197
197
|
var KYBER_SUITE = "ML-KEM-1024-FIPS203";
|
|
198
|
+
var KYBER_PUBLIC_KEY_SIZE = 1568;
|
|
199
|
+
var KYBER_SECRET_KEY_SIZE = 3168;
|
|
200
|
+
var KYBER_CIPHERTEXT_SIZE = 1568;
|
|
201
|
+
var KYBER_SHARED_SECRET_SIZE = 32;
|
|
202
|
+
var KYBER_SEED_SIZE = 64;
|
|
198
203
|
async function isKyberAvailable() {
|
|
199
204
|
return true;
|
|
200
205
|
}
|
|
@@ -250,8 +255,7 @@ async function loadDilithium() {
|
|
|
250
255
|
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa.js');
|
|
251
256
|
dilithiumModule = ml_dsa65;
|
|
252
257
|
return dilithiumModule;
|
|
253
|
-
} catch
|
|
254
|
-
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
258
|
+
} catch {
|
|
255
259
|
return null;
|
|
256
260
|
}
|
|
257
261
|
}
|
|
@@ -269,8 +273,7 @@ async function generateDilithiumKeypair() {
|
|
|
269
273
|
publicB64: toB64(kp.publicKey),
|
|
270
274
|
secretB64: toB64(kp.secretKey)
|
|
271
275
|
};
|
|
272
|
-
} catch
|
|
273
|
-
console.warn("[Dilithium] Key generation failed:", e);
|
|
276
|
+
} catch {
|
|
274
277
|
return null;
|
|
275
278
|
}
|
|
276
279
|
}
|
|
@@ -339,11 +342,13 @@ var DILITHIUM_SECRET_KEY_SIZE = 4032;
|
|
|
339
342
|
var DILITHIUM_SIGNATURE_SIZE = 3309;
|
|
340
343
|
var DILITHIUM_ALGORITHM = "ML-DSA-65";
|
|
341
344
|
var ENVELOPE_VERSION = OMNI_VERSIONS.HYBRID_V1;
|
|
345
|
+
var ENVELOPE_VERSION_V2 = OMNI_VERSIONS.HYBRID_V2;
|
|
342
346
|
var ENVELOPE_VERSION_LEGACY = DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
|
|
343
|
-
var
|
|
347
|
+
var ENVELOPE_SUITE_V2 = "x25519+mlkem1024";
|
|
344
348
|
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
345
349
|
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
346
350
|
ENVELOPE_VERSION,
|
|
351
|
+
ENVELOPE_VERSION_V2,
|
|
347
352
|
ENVELOPE_VERSION_LEGACY
|
|
348
353
|
];
|
|
349
354
|
var VersionMismatchError = class extends Error {
|
|
@@ -375,7 +380,6 @@ async function generateHybridIdentity(name) {
|
|
|
375
380
|
const x25519 = generateX25519Keypair();
|
|
376
381
|
const kyber = await generateKyberKeypair();
|
|
377
382
|
if (!kyber) {
|
|
378
|
-
console.error("Kyber key generation failed - library not available");
|
|
379
383
|
return null;
|
|
380
384
|
}
|
|
381
385
|
return {
|
|
@@ -417,49 +421,74 @@ function getSecretKeys(identity) {
|
|
|
417
421
|
kyberSecB64: identity.kyberSecB64
|
|
418
422
|
};
|
|
419
423
|
}
|
|
424
|
+
function combinedKekV2(kyberShared, x25519Shared, x25519EpkHex, kyberKemCtB64) {
|
|
425
|
+
const ikm = new Uint8Array(kyberShared.length + x25519Shared.length);
|
|
426
|
+
ikm.set(kyberShared, 0);
|
|
427
|
+
ikm.set(x25519Shared, kyberShared.length);
|
|
428
|
+
try {
|
|
429
|
+
return hkdfFlex(ikm, "omnituum/hybrid-v2", `wrap-ck|${x25519EpkHex}|${kyberKemCtB64}`);
|
|
430
|
+
} finally {
|
|
431
|
+
ikm.fill(0);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
420
434
|
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
421
435
|
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
422
436
|
const CK = rand32();
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
437
|
+
try {
|
|
438
|
+
const contentNonce = rand24();
|
|
439
|
+
const ciphertext = nacl.secretbox(pt, contentNonce, CK);
|
|
440
|
+
const x25519EphKp = nacl.box.keyPair();
|
|
441
|
+
const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
|
|
442
|
+
const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
443
|
+
const x25519EpkHex = toHex(x25519EphKp.publicKey);
|
|
444
|
+
const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
|
|
445
|
+
const kyberKemCtB64 = b64(kyberResult.ciphertext);
|
|
446
|
+
const kek = combinedKekV2(kyberResult.sharedSecret, x25519Shared, x25519EpkHex, kyberKemCtB64);
|
|
447
|
+
const ckWrapNonce = rand24();
|
|
448
|
+
const ckWrapped = nacl.secretbox(CK, ckWrapNonce, kek);
|
|
449
|
+
kek.fill(0);
|
|
450
|
+
x25519Shared.fill(0);
|
|
451
|
+
kyberResult.sharedSecret.fill(0);
|
|
452
|
+
x25519EphKp.secretKey.fill(0);
|
|
453
|
+
return {
|
|
454
|
+
v: ENVELOPE_VERSION_V2,
|
|
455
|
+
suite: ENVELOPE_SUITE_V2,
|
|
456
|
+
aead: ENVELOPE_AEAD,
|
|
457
|
+
x25519Epk: x25519EpkHex,
|
|
458
|
+
kyberKemCt: kyberKemCtB64,
|
|
459
|
+
ckWrap: {
|
|
460
|
+
nonce: b64(ckWrapNonce),
|
|
461
|
+
wrapped: b64(ckWrapped)
|
|
462
|
+
},
|
|
463
|
+
contentNonce: b64(contentNonce),
|
|
464
|
+
ciphertext: b64(ciphertext),
|
|
465
|
+
meta: {
|
|
466
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
467
|
+
senderName: sender?.name,
|
|
468
|
+
senderId: sender?.id
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
} finally {
|
|
472
|
+
CK.fill(0);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function unwrapCkV2(envelope, secretKeys) {
|
|
476
|
+
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
477
|
+
const ephPk = fromHex(envelope.x25519Epk);
|
|
478
|
+
const sk = fromHex(secretKeys.x25519SecHex);
|
|
479
|
+
const x25519Shared = nacl.scalarMult(sk, ephPk);
|
|
480
|
+
const kek = combinedKekV2(kyberShared, x25519Shared, envelope.x25519Epk, envelope.kyberKemCt);
|
|
481
|
+
kyberShared.fill(0);
|
|
482
|
+
x25519Shared.fill(0);
|
|
483
|
+
const CK = nacl.secretbox.open(ub64(envelope.ckWrap.wrapped), ub64(envelope.ckWrap.nonce), kek);
|
|
484
|
+
kek.fill(0);
|
|
485
|
+
if (!CK) {
|
|
486
|
+
throw new Error("Could not unwrap content key \u2014 combined-KEK authentication failed");
|
|
487
|
+
}
|
|
488
|
+
return CK;
|
|
457
489
|
}
|
|
458
|
-
async function
|
|
459
|
-
const version = envelope.v;
|
|
460
|
-
assertEnvelopeVersion(version);
|
|
490
|
+
async function unwrapCkV1(envelope, secretKeys, saltPrefix) {
|
|
461
491
|
let CK = null;
|
|
462
|
-
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
463
492
|
try {
|
|
464
493
|
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
465
494
|
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
@@ -468,11 +497,7 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
468
497
|
ub64(envelope.kyberWrap.nonce),
|
|
469
498
|
kyberKek
|
|
470
499
|
);
|
|
471
|
-
|
|
472
|
-
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
473
|
-
}
|
|
474
|
-
} catch (e) {
|
|
475
|
-
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
500
|
+
} catch {
|
|
476
501
|
}
|
|
477
502
|
if (!CK) {
|
|
478
503
|
try {
|
|
@@ -485,21 +510,28 @@ async function hybridDecrypt(envelope, secretKeys) {
|
|
|
485
510
|
ub64(envelope.x25519Wrap.nonce),
|
|
486
511
|
x25519Kek
|
|
487
512
|
);
|
|
488
|
-
|
|
489
|
-
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
490
|
-
}
|
|
491
|
-
} catch (e) {
|
|
492
|
-
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
513
|
+
} catch {
|
|
493
514
|
}
|
|
494
515
|
}
|
|
495
516
|
if (!CK) {
|
|
496
517
|
throw new Error("Could not unwrap content key with either algorithm");
|
|
497
518
|
}
|
|
519
|
+
return CK;
|
|
520
|
+
}
|
|
521
|
+
async function hybridDecrypt(envelope, secretKeys) {
|
|
522
|
+
const version = envelope.v;
|
|
523
|
+
assertEnvelopeVersion(version);
|
|
524
|
+
const CK = version === ENVELOPE_VERSION_V2 ? await unwrapCkV2(envelope, secretKeys) : await unwrapCkV1(
|
|
525
|
+
envelope,
|
|
526
|
+
secretKeys,
|
|
527
|
+
version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum"
|
|
528
|
+
);
|
|
498
529
|
const pt = nacl.secretbox.open(
|
|
499
530
|
ub64(envelope.ciphertext),
|
|
500
531
|
ub64(envelope.contentNonce),
|
|
501
532
|
CK
|
|
502
533
|
);
|
|
534
|
+
CK.fill(0);
|
|
503
535
|
if (!pt) {
|
|
504
536
|
throw new Error("Content authentication failed");
|
|
505
537
|
}
|
|
@@ -668,4 +700,4 @@ var HKDF_SHA512_OUTPUT_SIZE = 64;
|
|
|
668
700
|
var HKDF_SHA256_MAX_OUTPUT = 255 * 32;
|
|
669
701
|
var HKDF_SHA512_MAX_OUTPUT = 255 * 64;
|
|
670
702
|
|
|
671
|
-
export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, KYBER_SUITE, POLY1305_TAG_SIZE, SECRETBOX_KEY_SIZE, SECRETBOX_NONCE_SIZE, SECRETBOX_OVERHEAD, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
|
|
703
|
+
export { BLAKE3_BLOCK_SIZE, BLAKE3_KEY_LENGTH, BLAKE3_OUTPUT_LENGTH, BOX_KEY_SIZE, BOX_NONCE_SIZE, CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE, DILITHIUM_ALGORITHM, DILITHIUM_PUBLIC_KEY_SIZE, DILITHIUM_SECRET_KEY_SIZE, DILITHIUM_SIGNATURE_SIZE, HKDF_SHA256_MAX_OUTPUT, HKDF_SHA256_OUTPUT_SIZE, HKDF_SHA512_MAX_OUTPUT, HKDF_SHA512_OUTPUT_SIZE, 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, XCHACHA20_NONCE_SIZE, assertLen, b64, blake3, blake3DeriveKey, blake3Hex, blake3Mac, boxDecrypt, boxEncrypt, boxUnwrapWithX25519, boxWrapWithX25519, chaCha20Poly1305Decrypt, chaCha20Poly1305Encrypt, createChaCha20Poly1305, createXChaCha20Poly1305, deriveKeyFromShared, dilithiumSign, dilithiumSignRaw, dilithiumVerify, dilithiumVerifyRaw, fromB64, fromHex, generateDilithiumKeypair, generateDilithiumKeypairFromSeed, generateHybridIdentity, generateKyberKeypair, generateKyberKeypairFromSeed, generateX25519Keypair, generateX25519KeypairFromSeed, getPublicKeys, getSecretKeys, hkdfDerive, hkdfExpand, hkdfExtract, hkdfSha256, hkdfSplitForNoise, hkdfTripleSplitForNoise, hybridDecrypt, hybridDecryptToString, hybridEncrypt, isDilithiumAvailable, isKyberAvailable, kyberDecapsulate, kyberEncapsulate, kyberUnwrapKey, kyberWrapKey, rand12, rand24, rand32, randN, rotateHybridIdentity, secretboxDecrypt, secretboxDecryptString, secretboxEncrypt, secretboxEncryptString, secretboxOpenRaw, secretboxRaw, sha256, sha256String, textDecoder, textEncoder, toB64, toHex, u8, ub64, x25519SharedSecret, xChaCha20Poly1305Decrypt, xChaCha20Poly1305Encrypt };
|
|
@@ -8,16 +8,43 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Magic bytes: "OQEF" (Omnituum Quantum Encrypted File) */
|
|
10
10
|
declare const OQE_MAGIC: Uint8Array<ArrayBuffer>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* OQE format versions.
|
|
13
|
+
*
|
|
14
|
+
* v1 (LEGACY, read-only): a single AES-GCM IV was reused for both the
|
|
15
|
+
* metadata section and the content section under the same content key —
|
|
16
|
+
* catastrophic GCM nonce reuse — and hybrid mode wrapped the content key
|
|
17
|
+
* independently under X25519 and Kyber (either secret alone unwrapped it).
|
|
18
|
+
* Kept only so pre-existing files remain decryptable.
|
|
19
|
+
*
|
|
20
|
+
* v2 (CURRENT, write format): distinct random IVs per AES-GCM section, the
|
|
21
|
+
* serialized header bound as AEAD associated data, and hybrid mode wraps the
|
|
22
|
+
* content key once under an AND-combined KEK (HKDF(ss_mlkem || ss_x25519)
|
|
23
|
+
* with transcript binding) — both primitives must be broken to unwrap.
|
|
24
|
+
* See the 2026-07-06 fs security fix.
|
|
25
|
+
*/
|
|
26
|
+
declare const OQE_FORMAT_VERSION_V1 = 1;
|
|
27
|
+
declare const OQE_FORMAT_VERSION_V2 = 2;
|
|
28
|
+
/** Current (write) format version. */
|
|
29
|
+
declare const OQE_FORMAT_VERSION = 2;
|
|
30
|
+
/** Format versions this library can read. */
|
|
31
|
+
declare const SUPPORTED_OQE_VERSIONS: readonly [1, 2];
|
|
13
32
|
/** Supported encryption modes */
|
|
14
33
|
type OQEMode = 'hybrid' | 'password';
|
|
15
34
|
/** Algorithm suite identifiers */
|
|
16
35
|
declare const ALGORITHM_SUITES: {
|
|
17
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
|
|
38
|
+
* wraps (either secret unwraps). Only appears in v1 files. Never written.
|
|
39
|
+
*/
|
|
18
40
|
readonly HYBRID_X25519_KYBER768_AES256GCM: 1;
|
|
19
41
|
/** Password: Argon2id + AES-256-GCM */
|
|
20
42
|
readonly PASSWORD_ARGON2ID_AES256GCM: 2;
|
|
43
|
+
/**
|
|
44
|
+
* Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
|
|
45
|
+
* (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
|
|
46
|
+
*/
|
|
47
|
+
readonly HYBRID_X25519_MLKEM1024_AES256GCM: 3;
|
|
21
48
|
};
|
|
22
49
|
type AlgorithmSuiteId = typeof ALGORITHM_SUITES[keyof typeof ALGORITHM_SUITES];
|
|
23
50
|
interface Argon2idParams {
|
|
@@ -79,10 +106,25 @@ interface OQEHeader {
|
|
|
79
106
|
metadataLength: number;
|
|
80
107
|
/** Length of key material section */
|
|
81
108
|
keyMaterialLength: number;
|
|
82
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* AES-GCM IV for the metadata section. In v1 this same IV was (incorrectly)
|
|
111
|
+
* also used for the content section; v2 uses `contentIv` for content.
|
|
112
|
+
*/
|
|
83
113
|
iv: Uint8Array;
|
|
114
|
+
/**
|
|
115
|
+
* AES-GCM IV for the content section (v2 only). Distinct from `iv` so the
|
|
116
|
+
* two sections never share a (key, nonce) pair. Undefined for v1 files.
|
|
117
|
+
*/
|
|
118
|
+
contentIv?: Uint8Array;
|
|
84
119
|
}
|
|
85
|
-
/** Fixed header size in bytes (before variable-length sections) */
|
|
120
|
+
/** Fixed v1 header size in bytes (before variable-length sections). */
|
|
121
|
+
declare const OQE_HEADER_SIZE_V1 = 30;
|
|
122
|
+
/** Fixed v2 header size: v1 layout plus a 12-byte content IV. */
|
|
123
|
+
declare const OQE_HEADER_SIZE_V2 = 42;
|
|
124
|
+
/**
|
|
125
|
+
* @deprecated Use OQE_HEADER_SIZE_V1 / OQE_HEADER_SIZE_V2. Retained as the v1
|
|
126
|
+
* size for source compatibility.
|
|
127
|
+
*/
|
|
86
128
|
declare const OQE_HEADER_SIZE = 30;
|
|
87
129
|
/**
|
|
88
130
|
* Hybrid Mode Key Material:
|
|
@@ -107,6 +149,30 @@ interface HybridKeyMaterial {
|
|
|
107
149
|
/** Kyber wrap nonce */
|
|
108
150
|
kyberNonce: Uint8Array;
|
|
109
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* v2 Hybrid Mode Key Material — single AND-combined wrap.
|
|
154
|
+
*
|
|
155
|
+
* The content key is wrapped exactly once under a KEK derived from BOTH shared
|
|
156
|
+
* secrets: HKDF(ss_mlkem || ss_x25519) with the ephemeral X25519 key and the
|
|
157
|
+
* Kyber ciphertext bound into the info string. Both the X25519 and ML-KEM
|
|
158
|
+
* exchanges must succeed to unwrap — there is no per-primitive fallback.
|
|
159
|
+
*
|
|
160
|
+
* Serialized layout:
|
|
161
|
+
* - X25519 ephemeral public key (32 bytes)
|
|
162
|
+
* - Kyber KEM ciphertext (2-byte length prefix + data)
|
|
163
|
+
* - Content-key wrap nonce (24 bytes)
|
|
164
|
+
* - Wrapped content key (2-byte length prefix + data, 48 bytes)
|
|
165
|
+
*/
|
|
166
|
+
interface HybridKeyMaterialV2 {
|
|
167
|
+
/** X25519 ephemeral public key */
|
|
168
|
+
x25519EphemeralPk: Uint8Array;
|
|
169
|
+
/** Kyber KEM ciphertext */
|
|
170
|
+
kyberCiphertext: Uint8Array;
|
|
171
|
+
/** Wrap nonce for the single content-key wrap (NaCl secretbox) */
|
|
172
|
+
ckWrapNonce: Uint8Array;
|
|
173
|
+
/** Content key wrapped under the AND-combined KEK */
|
|
174
|
+
ckWrapped: Uint8Array;
|
|
175
|
+
}
|
|
110
176
|
/**
|
|
111
177
|
* Password Mode Key Material:
|
|
112
178
|
* - Argon2id salt (32 bytes)
|
|
@@ -209,6 +275,14 @@ declare function toUint8Array(input: FileInput): Promise<Uint8Array>;
|
|
|
209
275
|
* Outputs .oqe (Omnituum Quantum Encrypted) files.
|
|
210
276
|
*/
|
|
211
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Derive the v2 AND-combined KEK for file encryption. Requires BOTH shared
|
|
280
|
+
* secrets; the envelope's own KEM values are bound into the HKDF info so a
|
|
281
|
+
* spliced ephemeral key or Kyber ciphertext derives a different KEK and the
|
|
282
|
+
* wrap fails authentication (X-Wing-style transcript binding). Domain-separated
|
|
283
|
+
* from the message-envelope KEK via the "omnituum/fs/hybrid-v2" salt.
|
|
284
|
+
*/
|
|
285
|
+
declare function combinedFileKekV2(kyberShared: Uint8Array, x25519Shared: Uint8Array, x25519EpkHex: string, kyberKemCtB64: string): Uint8Array;
|
|
212
286
|
interface EncryptFileInput {
|
|
213
287
|
/** File data */
|
|
214
288
|
data: FileInput;
|
|
@@ -318,4 +392,4 @@ interface OQEFileInfo {
|
|
|
318
392
|
*/
|
|
319
393
|
declare function inspectOQEFile(data: FileInput): Promise<OQEFileInfo>;
|
|
320
394
|
|
|
321
|
-
export { type Argon2idParams as A, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, type HybridKeyMaterial as H, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type
|
|
395
|
+
export { type Argon2idParams as A, ALGORITHM_SUITES as B, OQE_HEADER_SIZE_V1 as C, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, OQE_HEADER_SIZE_V2 as G, type HybridKeyMaterial as H, OQE_HEADER_SIZE as I, toUint8Array as J, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, SUPPORTED_OQE_VERSIONS as S, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type HybridKeyMaterialV2 as g, type OQEMetadata as h, type AlgorithmSuiteId as i, DEFAULT_ARGON2ID_PARAMS as j, encryptFileForSelf as k, combinedFileKekV2 as l, decryptFileForSelf as m, inspectOQEFile as n, type OQEMode as o, type HybridEncryptOptions as p, type PasswordEncryptOptions as q, type HybridDecryptOptions as r, type PasswordDecryptOptions as s, type OQEErrorCode as t, type ProgressCallback as u, OQEError as v, OQE_MAGIC as w, OQE_FORMAT_VERSION_V1 as x, OQE_FORMAT_VERSION_V2 as y, OQE_FORMAT_VERSION as z };
|
|
@@ -8,16 +8,43 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Magic bytes: "OQEF" (Omnituum Quantum Encrypted File) */
|
|
10
10
|
declare const OQE_MAGIC: Uint8Array<ArrayBuffer>;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* OQE format versions.
|
|
13
|
+
*
|
|
14
|
+
* v1 (LEGACY, read-only): a single AES-GCM IV was reused for both the
|
|
15
|
+
* metadata section and the content section under the same content key —
|
|
16
|
+
* catastrophic GCM nonce reuse — and hybrid mode wrapped the content key
|
|
17
|
+
* independently under X25519 and Kyber (either secret alone unwrapped it).
|
|
18
|
+
* Kept only so pre-existing files remain decryptable.
|
|
19
|
+
*
|
|
20
|
+
* v2 (CURRENT, write format): distinct random IVs per AES-GCM section, the
|
|
21
|
+
* serialized header bound as AEAD associated data, and hybrid mode wraps the
|
|
22
|
+
* content key once under an AND-combined KEK (HKDF(ss_mlkem || ss_x25519)
|
|
23
|
+
* with transcript binding) — both primitives must be broken to unwrap.
|
|
24
|
+
* See the 2026-07-06 fs security fix.
|
|
25
|
+
*/
|
|
26
|
+
declare const OQE_FORMAT_VERSION_V1 = 1;
|
|
27
|
+
declare const OQE_FORMAT_VERSION_V2 = 2;
|
|
28
|
+
/** Current (write) format version. */
|
|
29
|
+
declare const OQE_FORMAT_VERSION = 2;
|
|
30
|
+
/** Format versions this library can read. */
|
|
31
|
+
declare const SUPPORTED_OQE_VERSIONS: readonly [1, 2];
|
|
13
32
|
/** Supported encryption modes */
|
|
14
33
|
type OQEMode = 'hybrid' | 'password';
|
|
15
34
|
/** Algorithm suite identifiers */
|
|
16
35
|
declare const ALGORITHM_SUITES: {
|
|
17
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* LEGACY (read-only): Hybrid X25519 + Kyber with independent per-primitive
|
|
38
|
+
* wraps (either secret unwraps). Only appears in v1 files. Never written.
|
|
39
|
+
*/
|
|
18
40
|
readonly HYBRID_X25519_KYBER768_AES256GCM: 1;
|
|
19
41
|
/** Password: Argon2id + AES-256-GCM */
|
|
20
42
|
readonly PASSWORD_ARGON2ID_AES256GCM: 2;
|
|
43
|
+
/**
|
|
44
|
+
* Hybrid X25519 + ML-KEM-1024 with a single AND-combined KEK wrap
|
|
45
|
+
* (HKDF(ss_mlkem || ss_x25519), transcript-bound). Written by v2.
|
|
46
|
+
*/
|
|
47
|
+
readonly HYBRID_X25519_MLKEM1024_AES256GCM: 3;
|
|
21
48
|
};
|
|
22
49
|
type AlgorithmSuiteId = typeof ALGORITHM_SUITES[keyof typeof ALGORITHM_SUITES];
|
|
23
50
|
interface Argon2idParams {
|
|
@@ -79,10 +106,25 @@ interface OQEHeader {
|
|
|
79
106
|
metadataLength: number;
|
|
80
107
|
/** Length of key material section */
|
|
81
108
|
keyMaterialLength: number;
|
|
82
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* AES-GCM IV for the metadata section. In v1 this same IV was (incorrectly)
|
|
111
|
+
* also used for the content section; v2 uses `contentIv` for content.
|
|
112
|
+
*/
|
|
83
113
|
iv: Uint8Array;
|
|
114
|
+
/**
|
|
115
|
+
* AES-GCM IV for the content section (v2 only). Distinct from `iv` so the
|
|
116
|
+
* two sections never share a (key, nonce) pair. Undefined for v1 files.
|
|
117
|
+
*/
|
|
118
|
+
contentIv?: Uint8Array;
|
|
84
119
|
}
|
|
85
|
-
/** Fixed header size in bytes (before variable-length sections) */
|
|
120
|
+
/** Fixed v1 header size in bytes (before variable-length sections). */
|
|
121
|
+
declare const OQE_HEADER_SIZE_V1 = 30;
|
|
122
|
+
/** Fixed v2 header size: v1 layout plus a 12-byte content IV. */
|
|
123
|
+
declare const OQE_HEADER_SIZE_V2 = 42;
|
|
124
|
+
/**
|
|
125
|
+
* @deprecated Use OQE_HEADER_SIZE_V1 / OQE_HEADER_SIZE_V2. Retained as the v1
|
|
126
|
+
* size for source compatibility.
|
|
127
|
+
*/
|
|
86
128
|
declare const OQE_HEADER_SIZE = 30;
|
|
87
129
|
/**
|
|
88
130
|
* Hybrid Mode Key Material:
|
|
@@ -107,6 +149,30 @@ interface HybridKeyMaterial {
|
|
|
107
149
|
/** Kyber wrap nonce */
|
|
108
150
|
kyberNonce: Uint8Array;
|
|
109
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* v2 Hybrid Mode Key Material — single AND-combined wrap.
|
|
154
|
+
*
|
|
155
|
+
* The content key is wrapped exactly once under a KEK derived from BOTH shared
|
|
156
|
+
* secrets: HKDF(ss_mlkem || ss_x25519) with the ephemeral X25519 key and the
|
|
157
|
+
* Kyber ciphertext bound into the info string. Both the X25519 and ML-KEM
|
|
158
|
+
* exchanges must succeed to unwrap — there is no per-primitive fallback.
|
|
159
|
+
*
|
|
160
|
+
* Serialized layout:
|
|
161
|
+
* - X25519 ephemeral public key (32 bytes)
|
|
162
|
+
* - Kyber KEM ciphertext (2-byte length prefix + data)
|
|
163
|
+
* - Content-key wrap nonce (24 bytes)
|
|
164
|
+
* - Wrapped content key (2-byte length prefix + data, 48 bytes)
|
|
165
|
+
*/
|
|
166
|
+
interface HybridKeyMaterialV2 {
|
|
167
|
+
/** X25519 ephemeral public key */
|
|
168
|
+
x25519EphemeralPk: Uint8Array;
|
|
169
|
+
/** Kyber KEM ciphertext */
|
|
170
|
+
kyberCiphertext: Uint8Array;
|
|
171
|
+
/** Wrap nonce for the single content-key wrap (NaCl secretbox) */
|
|
172
|
+
ckWrapNonce: Uint8Array;
|
|
173
|
+
/** Content key wrapped under the AND-combined KEK */
|
|
174
|
+
ckWrapped: Uint8Array;
|
|
175
|
+
}
|
|
110
176
|
/**
|
|
111
177
|
* Password Mode Key Material:
|
|
112
178
|
* - Argon2id salt (32 bytes)
|
|
@@ -209,6 +275,14 @@ declare function toUint8Array(input: FileInput): Promise<Uint8Array>;
|
|
|
209
275
|
* Outputs .oqe (Omnituum Quantum Encrypted) files.
|
|
210
276
|
*/
|
|
211
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Derive the v2 AND-combined KEK for file encryption. Requires BOTH shared
|
|
280
|
+
* secrets; the envelope's own KEM values are bound into the HKDF info so a
|
|
281
|
+
* spliced ephemeral key or Kyber ciphertext derives a different KEK and the
|
|
282
|
+
* wrap fails authentication (X-Wing-style transcript binding). Domain-separated
|
|
283
|
+
* from the message-envelope KEK via the "omnituum/fs/hybrid-v2" salt.
|
|
284
|
+
*/
|
|
285
|
+
declare function combinedFileKekV2(kyberShared: Uint8Array, x25519Shared: Uint8Array, x25519EpkHex: string, kyberKemCtB64: string): Uint8Array;
|
|
212
286
|
interface EncryptFileInput {
|
|
213
287
|
/** File data */
|
|
214
288
|
data: FileInput;
|
|
@@ -318,4 +392,4 @@ interface OQEFileInfo {
|
|
|
318
392
|
*/
|
|
319
393
|
declare function inspectOQEFile(data: FileInput): Promise<OQEFileInfo>;
|
|
320
394
|
|
|
321
|
-
export { type Argon2idParams as A, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, type HybridKeyMaterial as H, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type
|
|
395
|
+
export { type Argon2idParams as A, ALGORITHM_SUITES as B, OQE_HEADER_SIZE_V1 as C, type DecryptOptions as D, type EncryptOptions as E, type FileInput as F, OQE_HEADER_SIZE_V2 as G, type HybridKeyMaterial as H, OQE_HEADER_SIZE as I, toUint8Array as J, MIN_ARGON2ID_PARAMS as M, type OQEEncryptResult as O, type PasswordKeyMaterial as P, SUPPORTED_OQE_VERSIONS as S, encryptFileWithPassword as a, decryptFileWithPassword as b, type OQEDecryptResult as c, decryptFile as d, encryptFile as e, type OQEHeader as f, type HybridKeyMaterialV2 as g, type OQEMetadata as h, type AlgorithmSuiteId as i, DEFAULT_ARGON2ID_PARAMS as j, encryptFileForSelf as k, combinedFileKekV2 as l, decryptFileForSelf as m, inspectOQEFile as n, type OQEMode as o, type HybridEncryptOptions as p, type PasswordEncryptOptions as q, type HybridDecryptOptions as r, type PasswordDecryptOptions as s, type OQEErrorCode as t, type ProgressCallback as u, OQEError as v, OQE_MAGIC as w, OQE_FORMAT_VERSION_V1 as x, OQE_FORMAT_VERSION_V2 as y, OQE_FORMAT_VERSION as z };
|