@omnituum/pqc-shared 0.2.6
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/LICENSE +22 -0
- package/README.md +543 -0
- package/dist/crypto/index.cjs +807 -0
- package/dist/crypto/index.d.cts +641 -0
- package/dist/crypto/index.d.ts +641 -0
- package/dist/crypto/index.js +716 -0
- package/dist/decrypt-eSHlbh1j.d.cts +321 -0
- package/dist/decrypt-eSHlbh1j.d.ts +321 -0
- package/dist/fs/index.cjs +1168 -0
- package/dist/fs/index.d.cts +400 -0
- package/dist/fs/index.d.ts +400 -0
- package/dist/fs/index.js +1091 -0
- package/dist/index.cjs +2160 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +2031 -0
- package/dist/integrity-CCYjrap3.d.ts +31 -0
- package/dist/integrity-Dx9jukMH.d.cts +31 -0
- package/dist/types-61c7Q9ri.d.ts +134 -0
- package/dist/types-Ch0y-n7K.d.cts +134 -0
- package/dist/utils/index.cjs +129 -0
- package/dist/utils/index.d.cts +49 -0
- package/dist/utils/index.d.ts +49 -0
- package/dist/utils/index.js +114 -0
- package/dist/vault/index.cjs +713 -0
- package/dist/vault/index.d.cts +237 -0
- package/dist/vault/index.d.ts +237 -0
- package/dist/vault/index.js +677 -0
- package/dist/version-BygzPVGs.d.cts +55 -0
- package/dist/version-BygzPVGs.d.ts +55 -0
- package/package.json +86 -0
- package/src/crypto/dilithium.ts +233 -0
- package/src/crypto/hybrid.ts +358 -0
- package/src/crypto/index.ts +181 -0
- package/src/crypto/kyber.ts +199 -0
- package/src/crypto/nacl.ts +204 -0
- package/src/crypto/primitives/blake3.ts +141 -0
- package/src/crypto/primitives/chacha.ts +211 -0
- package/src/crypto/primitives/hkdf.ts +192 -0
- package/src/crypto/primitives/index.ts +54 -0
- package/src/crypto/primitives.ts +144 -0
- package/src/crypto/x25519.ts +134 -0
- package/src/fs/aes.ts +343 -0
- package/src/fs/argon2.ts +184 -0
- package/src/fs/browser.ts +408 -0
- package/src/fs/decrypt.ts +320 -0
- package/src/fs/encrypt.ts +324 -0
- package/src/fs/format.ts +425 -0
- package/src/fs/index.ts +144 -0
- package/src/fs/types.ts +304 -0
- package/src/index.ts +414 -0
- package/src/kdf/index.ts +311 -0
- package/src/runtime/crypto.ts +16 -0
- package/src/security/index.ts +345 -0
- package/src/tunnel/index.ts +39 -0
- package/src/tunnel/session.ts +229 -0
- package/src/tunnel/types.ts +115 -0
- package/src/utils/entropy.ts +128 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/integrity.ts +95 -0
- package/src/vault/decrypt.ts +167 -0
- package/src/vault/encrypt.ts +207 -0
- package/src/vault/index.ts +71 -0
- package/src/vault/manager.ts +327 -0
- package/src/vault/migrate.ts +190 -0
- package/src/vault/types.ts +177 -0
- package/src/version.ts +304 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { sha256 as sha256$1 } from '@noble/hashes/sha256';
|
|
2
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
3
|
+
import nacl from 'tweetnacl';
|
|
4
|
+
import { blake3 as blake3$1 } from '@noble/hashes/blake3';
|
|
5
|
+
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha';
|
|
6
|
+
import { hkdf, extract, expand } from '@noble/hashes/hkdf';
|
|
7
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
8
|
+
|
|
9
|
+
// src/crypto/primitives.ts
|
|
10
|
+
var textEncoder = new TextEncoder();
|
|
11
|
+
var textDecoder = new TextDecoder();
|
|
12
|
+
function toB64(bytes) {
|
|
13
|
+
let binary = "";
|
|
14
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
15
|
+
binary += String.fromCharCode(bytes[i]);
|
|
16
|
+
}
|
|
17
|
+
return btoa(binary);
|
|
18
|
+
}
|
|
19
|
+
function fromB64(str) {
|
|
20
|
+
const binary = atob(str);
|
|
21
|
+
const bytes = new Uint8Array(binary.length);
|
|
22
|
+
for (let i = 0; i < binary.length; i++) {
|
|
23
|
+
bytes[i] = binary.charCodeAt(i);
|
|
24
|
+
}
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
function toHex(bytes) {
|
|
28
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
30
|
+
function fromHex(hex) {
|
|
31
|
+
const s = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
32
|
+
const normalized = s.length % 2 ? "0" + s : s;
|
|
33
|
+
const out = new Uint8Array(normalized.length / 2);
|
|
34
|
+
for (let i = 0; i < out.length; i++) {
|
|
35
|
+
out[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function assertLen(label, arr, n) {
|
|
40
|
+
if (arr.length !== n) {
|
|
41
|
+
throw new Error(`${label} must be ${n} bytes, got ${arr.length}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function rand32() {
|
|
45
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
46
|
+
}
|
|
47
|
+
function rand24() {
|
|
48
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(24));
|
|
49
|
+
}
|
|
50
|
+
function rand12() {
|
|
51
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
52
|
+
}
|
|
53
|
+
function randN(n) {
|
|
54
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
55
|
+
}
|
|
56
|
+
function sha256(bytes) {
|
|
57
|
+
return sha256$1(bytes);
|
|
58
|
+
}
|
|
59
|
+
function sha256String(str) {
|
|
60
|
+
return sha256(textEncoder.encode(str));
|
|
61
|
+
}
|
|
62
|
+
function hkdfSha256(ikm, opts) {
|
|
63
|
+
const salt = opts?.salt ?? new Uint8Array(32);
|
|
64
|
+
const info = opts?.info ?? new Uint8Array(0);
|
|
65
|
+
const L = opts?.length ?? 32;
|
|
66
|
+
const prk = hmac(sha256$1, salt, ikm);
|
|
67
|
+
let t = new Uint8Array(0);
|
|
68
|
+
const chunks = [];
|
|
69
|
+
for (let i = 1; i <= Math.ceil(L / 32); i++) {
|
|
70
|
+
const input = new Uint8Array(t.length + info.length + 1);
|
|
71
|
+
input.set(t, 0);
|
|
72
|
+
input.set(info, t.length);
|
|
73
|
+
input[input.length - 1] = i;
|
|
74
|
+
t = new Uint8Array(hmac(sha256$1, prk, input));
|
|
75
|
+
chunks.push(t);
|
|
76
|
+
}
|
|
77
|
+
const out = new Uint8Array(L);
|
|
78
|
+
let off = 0;
|
|
79
|
+
for (const c of chunks) {
|
|
80
|
+
out.set(c.subarray(0, L - off), off);
|
|
81
|
+
off += c.length;
|
|
82
|
+
if (off >= L) break;
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
var b64 = toB64;
|
|
87
|
+
var ub64 = fromB64;
|
|
88
|
+
var u8 = (s) => typeof s === "string" ? textEncoder.encode(s) : s;
|
|
89
|
+
function secretboxEncrypt(key, plaintext, nonce) {
|
|
90
|
+
assertLen("secretbox key", key, 32);
|
|
91
|
+
const n = nonce ?? rand24();
|
|
92
|
+
assertLen("nonce", n, 24);
|
|
93
|
+
const ciphertext = nacl.secretbox(plaintext, n, key);
|
|
94
|
+
return {
|
|
95
|
+
scheme: "nacl.secretbox",
|
|
96
|
+
nonce: toB64(n),
|
|
97
|
+
ciphertext: toB64(ciphertext)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function secretboxEncryptString(key, plaintext, nonce) {
|
|
101
|
+
return secretboxEncrypt(key, new TextEncoder().encode(plaintext), nonce);
|
|
102
|
+
}
|
|
103
|
+
function secretboxDecrypt(key, payload) {
|
|
104
|
+
if (payload.scheme !== "nacl.secretbox") {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
assertLen("secretbox key", key, 32);
|
|
108
|
+
const nonce = fromB64(payload.nonce);
|
|
109
|
+
const ciphertext = fromB64(payload.ciphertext);
|
|
110
|
+
return nacl.secretbox.open(ciphertext, nonce, key) || null;
|
|
111
|
+
}
|
|
112
|
+
function secretboxDecryptString(key, payload) {
|
|
113
|
+
const result = secretboxDecrypt(key, payload);
|
|
114
|
+
if (!result) return null;
|
|
115
|
+
return new TextDecoder().decode(result);
|
|
116
|
+
}
|
|
117
|
+
function secretboxRaw(key, plaintext, nonce) {
|
|
118
|
+
assertLen("secretbox key", key, 32);
|
|
119
|
+
assertLen("nonce", nonce, 24);
|
|
120
|
+
return nacl.secretbox(plaintext, nonce, key);
|
|
121
|
+
}
|
|
122
|
+
function secretboxOpenRaw(key, ciphertext, nonce) {
|
|
123
|
+
assertLen("secretbox key", key, 32);
|
|
124
|
+
assertLen("nonce", nonce, 24);
|
|
125
|
+
return nacl.secretbox.open(ciphertext, nonce, key) || null;
|
|
126
|
+
}
|
|
127
|
+
function boxEncrypt(plaintext, nonce, recipientPubKey, senderSecKey) {
|
|
128
|
+
assertLen("nonce", nonce, 24);
|
|
129
|
+
assertLen("recipient pubKey", recipientPubKey, 32);
|
|
130
|
+
assertLen("sender secKey", senderSecKey, 32);
|
|
131
|
+
return nacl.box(plaintext, nonce, recipientPubKey, senderSecKey);
|
|
132
|
+
}
|
|
133
|
+
function boxDecrypt(ciphertext, nonce, senderPubKey, recipientSecKey) {
|
|
134
|
+
assertLen("nonce", nonce, 24);
|
|
135
|
+
assertLen("sender pubKey", senderPubKey, 32);
|
|
136
|
+
assertLen("recipient secKey", recipientSecKey, 32);
|
|
137
|
+
return nacl.box.open(ciphertext, nonce, senderPubKey, recipientSecKey) || null;
|
|
138
|
+
}
|
|
139
|
+
var SECRETBOX_KEY_SIZE = nacl.secretbox.keyLength;
|
|
140
|
+
var SECRETBOX_NONCE_SIZE = nacl.secretbox.nonceLength;
|
|
141
|
+
var SECRETBOX_OVERHEAD = nacl.secretbox.overheadLength;
|
|
142
|
+
var BOX_KEY_SIZE = nacl.box.publicKeyLength;
|
|
143
|
+
var BOX_NONCE_SIZE = nacl.box.nonceLength;
|
|
144
|
+
function generateX25519Keypair() {
|
|
145
|
+
const kp = nacl.box.keyPair();
|
|
146
|
+
return {
|
|
147
|
+
publicHex: "0x" + toHex(kp.publicKey),
|
|
148
|
+
secretHex: "0x" + toHex(kp.secretKey),
|
|
149
|
+
publicBytes: kp.publicKey,
|
|
150
|
+
secretBytes: kp.secretKey
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function generateX25519KeypairFromSeed(seed) {
|
|
154
|
+
if (seed.length !== 32) {
|
|
155
|
+
throw new Error("X25519 seed must be 32 bytes");
|
|
156
|
+
}
|
|
157
|
+
const uniformSeed = sha256(seed);
|
|
158
|
+
const kp = nacl.box.keyPair.fromSecretKey(uniformSeed);
|
|
159
|
+
return {
|
|
160
|
+
publicKey: kp.publicKey,
|
|
161
|
+
secretKey: uniformSeed
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function boxWrapWithX25519(symKey32, recipientPubKeyHex) {
|
|
165
|
+
assertLen("sym key", symKey32, 32);
|
|
166
|
+
const pk = fromHex(recipientPubKeyHex);
|
|
167
|
+
assertLen("recipient pubKey", pk, 32);
|
|
168
|
+
const eph = nacl.box.keyPair();
|
|
169
|
+
const nonce = rand24();
|
|
170
|
+
const boxed = nacl.box(symKey32, nonce, pk, eph.secretKey);
|
|
171
|
+
return {
|
|
172
|
+
ephPubKey: toHex(eph.publicKey),
|
|
173
|
+
nonce: b64(nonce),
|
|
174
|
+
boxed: b64(boxed)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function boxUnwrapWithX25519(wrap, recipientSecretKey32) {
|
|
178
|
+
assertLen("recipient secretKey", recipientSecretKey32, 32);
|
|
179
|
+
const ephPk = fromHex(wrap.ephPubKey);
|
|
180
|
+
assertLen("ephemeral pubKey", ephPk, 32);
|
|
181
|
+
return nacl.box.open(
|
|
182
|
+
ub64(wrap.boxed),
|
|
183
|
+
ub64(wrap.nonce),
|
|
184
|
+
ephPk,
|
|
185
|
+
recipientSecretKey32
|
|
186
|
+
) || null;
|
|
187
|
+
}
|
|
188
|
+
function x25519SharedSecret(ourSecretKey, theirPublicKey) {
|
|
189
|
+
assertLen("secret key", ourSecretKey, 32);
|
|
190
|
+
assertLen("public key", theirPublicKey, 32);
|
|
191
|
+
return nacl.scalarMult(ourSecretKey, theirPublicKey);
|
|
192
|
+
}
|
|
193
|
+
function deriveKeyFromShared(shared, salt, info) {
|
|
194
|
+
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
195
|
+
}
|
|
196
|
+
var kyberModule = null;
|
|
197
|
+
async function loadKyber() {
|
|
198
|
+
if (kyberModule) return kyberModule;
|
|
199
|
+
try {
|
|
200
|
+
const m = await import('kyber-crystals');
|
|
201
|
+
const k = m.default ?? m;
|
|
202
|
+
kyberModule = k.kyber ?? k;
|
|
203
|
+
return kyberModule;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.warn("[Kyber] Failed to load kyber-crystals:", e);
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function isKyberAvailable() {
|
|
210
|
+
const mod = await loadKyber();
|
|
211
|
+
return mod !== null;
|
|
212
|
+
}
|
|
213
|
+
async function generateKyberKeypair() {
|
|
214
|
+
try {
|
|
215
|
+
const mod = await loadKyber();
|
|
216
|
+
if (!mod) return null;
|
|
217
|
+
if (mod?.ready?.then) {
|
|
218
|
+
await mod.ready;
|
|
219
|
+
}
|
|
220
|
+
const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
|
|
221
|
+
if (typeof fn !== "function") {
|
|
222
|
+
console.warn("[Kyber] No keypair function found");
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const kp = await fn.call(mod);
|
|
226
|
+
const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
|
|
227
|
+
const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
|
|
228
|
+
if (!pub || !priv) {
|
|
229
|
+
console.warn("[Kyber] Invalid keypair result");
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
publicB64: b64(new Uint8Array(pub)),
|
|
234
|
+
secretB64: b64(new Uint8Array(priv))
|
|
235
|
+
};
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.warn("[Kyber] Key generation failed:", e);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function kyberEncapsulate(pubKeyB64) {
|
|
242
|
+
const kyber = await loadKyber();
|
|
243
|
+
if (!kyber?.encrypt) {
|
|
244
|
+
throw new Error("Kyber encrypt not available");
|
|
245
|
+
}
|
|
246
|
+
const pk = ub64(pubKeyB64);
|
|
247
|
+
const r = await kyber.encrypt(pk);
|
|
248
|
+
const ctRaw = r?.ciphertext ?? r?.cyphertext ?? r?.ct ?? r?.bytes?.ciphertext ?? r?.bytes?.cyphertext ?? r?.bytes?.ct ?? (Array.isArray(r) ? r[0] : void 0);
|
|
249
|
+
const ssRaw = r?.key ?? r?.sharedSecret ?? r?.secret ?? r?.bytes?.key ?? r?.bytes?.sharedSecret ?? (Array.isArray(r) ? r[1] : void 0);
|
|
250
|
+
if (!ctRaw || !ssRaw) {
|
|
251
|
+
throw new Error("Kyber encapsulate failed: missing ciphertext or shared secret");
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
ciphertext: new Uint8Array(ctRaw),
|
|
255
|
+
sharedSecret: new Uint8Array(ssRaw)
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function kyberDecapsulate(kemCiphertextB64, secretKeyB64) {
|
|
259
|
+
const kyber = await loadKyber();
|
|
260
|
+
if (!kyber?.decrypt && !kyber?.decapsulate) {
|
|
261
|
+
throw new Error("Kyber decrypt/decapsulate not available");
|
|
262
|
+
}
|
|
263
|
+
const ct = ub64(kemCiphertextB64);
|
|
264
|
+
const sk = ub64(secretKeyB64);
|
|
265
|
+
const r = kyber.decrypt ? await kyber.decrypt(ct, sk) : await kyber.decapsulate(ct, sk);
|
|
266
|
+
const key = r && (r.key ?? r.sharedSecret) ? r.key ?? r.sharedSecret : r;
|
|
267
|
+
return new Uint8Array(key);
|
|
268
|
+
}
|
|
269
|
+
function kyberWrapKey(sharedSecret, msgKey32) {
|
|
270
|
+
if (msgKey32.length !== 32) {
|
|
271
|
+
throw new Error("Message key must be 32 bytes");
|
|
272
|
+
}
|
|
273
|
+
const kek = sha256(sharedSecret);
|
|
274
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(24));
|
|
275
|
+
const wrapped = nacl.secretbox(msgKey32, nonce, kek);
|
|
276
|
+
return {
|
|
277
|
+
nonce: b64(nonce),
|
|
278
|
+
wrapped: b64(wrapped)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
282
|
+
const kek = sha256(sharedSecret);
|
|
283
|
+
const nonce = ub64(nonceB64);
|
|
284
|
+
const wrapped = ub64(wrappedB64);
|
|
285
|
+
return nacl.secretbox.open(wrapped, nonce, kek) || null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/crypto/dilithium.ts
|
|
289
|
+
var dilithiumModule = null;
|
|
290
|
+
async function loadDilithium() {
|
|
291
|
+
if (dilithiumModule) return dilithiumModule;
|
|
292
|
+
try {
|
|
293
|
+
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa');
|
|
294
|
+
dilithiumModule = ml_dsa65;
|
|
295
|
+
return dilithiumModule;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function isDilithiumAvailable() {
|
|
302
|
+
const mod = await loadDilithium();
|
|
303
|
+
return mod !== null;
|
|
304
|
+
}
|
|
305
|
+
async function generateDilithiumKeypair() {
|
|
306
|
+
try {
|
|
307
|
+
const mod = await loadDilithium();
|
|
308
|
+
if (!mod) return null;
|
|
309
|
+
const seed = randN(32);
|
|
310
|
+
const kp = mod.keygen(seed);
|
|
311
|
+
return {
|
|
312
|
+
publicB64: toB64(kp.publicKey),
|
|
313
|
+
secretB64: toB64(kp.secretKey)
|
|
314
|
+
};
|
|
315
|
+
} catch (e) {
|
|
316
|
+
console.warn("[Dilithium] Key generation failed:", e);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function generateDilithiumKeypairFromSeed(seed) {
|
|
321
|
+
const mod = await loadDilithium();
|
|
322
|
+
if (!mod) {
|
|
323
|
+
throw new Error("Dilithium library not available");
|
|
324
|
+
}
|
|
325
|
+
if (seed.length !== 32) {
|
|
326
|
+
throw new Error("Dilithium seed must be 32 bytes");
|
|
327
|
+
}
|
|
328
|
+
const uniformSeed = sha256(seed);
|
|
329
|
+
const kp = mod.keygen(uniformSeed);
|
|
330
|
+
return {
|
|
331
|
+
publicKey: kp.publicKey,
|
|
332
|
+
secretKey: kp.secretKey
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function dilithiumSign(message, secretKeyB64) {
|
|
336
|
+
const mod = await loadDilithium();
|
|
337
|
+
if (!mod) {
|
|
338
|
+
throw new Error("Dilithium library not available");
|
|
339
|
+
}
|
|
340
|
+
const sk = fromB64(secretKeyB64);
|
|
341
|
+
const msg = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
342
|
+
const signature = mod.sign(sk, msg);
|
|
343
|
+
return {
|
|
344
|
+
signature: toB64(signature),
|
|
345
|
+
algorithm: "ML-DSA-65"
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function dilithiumSignRaw(message, secretKey) {
|
|
349
|
+
const mod = await loadDilithium();
|
|
350
|
+
if (!mod) {
|
|
351
|
+
throw new Error("Dilithium library not available");
|
|
352
|
+
}
|
|
353
|
+
return mod.sign(secretKey, message);
|
|
354
|
+
}
|
|
355
|
+
async function dilithiumVerify(message, signatureB64, publicKeyB64) {
|
|
356
|
+
const mod = await loadDilithium();
|
|
357
|
+
if (!mod) {
|
|
358
|
+
throw new Error("Dilithium library not available");
|
|
359
|
+
}
|
|
360
|
+
const pk = fromB64(publicKeyB64);
|
|
361
|
+
const sig = fromB64(signatureB64);
|
|
362
|
+
const msg = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
363
|
+
try {
|
|
364
|
+
return mod.verify(pk, msg, sig);
|
|
365
|
+
} catch {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function dilithiumVerifyRaw(message, signature, publicKey) {
|
|
370
|
+
const mod = await loadDilithium();
|
|
371
|
+
if (!mod) {
|
|
372
|
+
throw new Error("Dilithium library not available");
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
return mod.verify(publicKey, message, signature);
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
var DILITHIUM_PUBLIC_KEY_SIZE = 1952;
|
|
381
|
+
var DILITHIUM_SECRET_KEY_SIZE = 4032;
|
|
382
|
+
var DILITHIUM_SIGNATURE_SIZE = 3309;
|
|
383
|
+
var DILITHIUM_ALGORITHM = "ML-DSA-65";
|
|
384
|
+
|
|
385
|
+
// src/version.ts
|
|
386
|
+
var ENVELOPE_VERSION = "omnituum.hybrid.v1";
|
|
387
|
+
var ENVELOPE_VERSION_LEGACY = "pqc-demo.hybrid.v1";
|
|
388
|
+
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
389
|
+
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
390
|
+
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
391
|
+
ENVELOPE_VERSION,
|
|
392
|
+
ENVELOPE_VERSION_LEGACY
|
|
393
|
+
];
|
|
394
|
+
var VersionMismatchError = class extends Error {
|
|
395
|
+
constructor(type, expected, received) {
|
|
396
|
+
super(
|
|
397
|
+
`Version mismatch for ${type}: expected one of [${expected.join(", ")}], got "${received}". This may indicate data corruption or a newer format. See pqc-docs/specs/ for format specifications.`
|
|
398
|
+
);
|
|
399
|
+
this.type = type;
|
|
400
|
+
this.expected = expected;
|
|
401
|
+
this.received = received;
|
|
402
|
+
this.name = "VersionMismatchError";
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
function assertEnvelopeVersion(version) {
|
|
406
|
+
if (!SUPPORTED_ENVELOPE_VERSIONS.includes(version)) {
|
|
407
|
+
throw new VersionMismatchError("envelope", SUPPORTED_ENVELOPE_VERSIONS, version);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/crypto/hybrid.ts
|
|
412
|
+
function hkdfFlex(ikm, salt, info) {
|
|
413
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
414
|
+
}
|
|
415
|
+
function generateId() {
|
|
416
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
417
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
418
|
+
}
|
|
419
|
+
async function generateHybridIdentity(name) {
|
|
420
|
+
const x25519 = generateX25519Keypair();
|
|
421
|
+
const kyber = await generateKyberKeypair();
|
|
422
|
+
if (!kyber) {
|
|
423
|
+
console.error("Kyber key generation failed - library not available");
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
id: generateId(),
|
|
428
|
+
name,
|
|
429
|
+
x25519PubHex: x25519.publicHex,
|
|
430
|
+
x25519SecHex: x25519.secretHex,
|
|
431
|
+
kyberPubB64: kyber.publicB64,
|
|
432
|
+
kyberSecB64: kyber.secretB64,
|
|
433
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
rotationCount: 0
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function rotateHybridIdentity(identity) {
|
|
438
|
+
const x25519 = generateX25519Keypair();
|
|
439
|
+
const kyber = await generateKyberKeypair();
|
|
440
|
+
if (!kyber) {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
...identity,
|
|
445
|
+
x25519PubHex: x25519.publicHex,
|
|
446
|
+
x25519SecHex: x25519.secretHex,
|
|
447
|
+
kyberPubB64: kyber.publicB64,
|
|
448
|
+
kyberSecB64: kyber.secretB64,
|
|
449
|
+
lastRotatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
450
|
+
rotationCount: identity.rotationCount + 1
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function getPublicKeys(identity) {
|
|
454
|
+
return {
|
|
455
|
+
x25519PubHex: identity.x25519PubHex,
|
|
456
|
+
kyberPubB64: identity.kyberPubB64
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function getSecretKeys(identity) {
|
|
460
|
+
return {
|
|
461
|
+
x25519SecHex: identity.x25519SecHex,
|
|
462
|
+
kyberSecB64: identity.kyberSecB64
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
466
|
+
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
467
|
+
const CK = rand32();
|
|
468
|
+
const contentNonce = rand24();
|
|
469
|
+
const ciphertext = nacl.secretbox(pt, contentNonce, CK);
|
|
470
|
+
const x25519EphKp = nacl.box.keyPair();
|
|
471
|
+
const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
|
|
472
|
+
const x25519Shared = nacl.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
473
|
+
const x25519Kek = hkdfFlex(x25519Shared, "omnituum/x25519", "wrap-ck");
|
|
474
|
+
const x25519WrapNonce = rand24();
|
|
475
|
+
const x25519Wrapped = nacl.secretbox(CK, x25519WrapNonce, x25519Kek);
|
|
476
|
+
const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
|
|
477
|
+
const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/kyber", "wrap-ck");
|
|
478
|
+
const kyberWrapNonce = rand24();
|
|
479
|
+
const kyberWrapped = nacl.secretbox(CK, kyberWrapNonce, kyberKek);
|
|
480
|
+
return {
|
|
481
|
+
v: ENVELOPE_VERSION,
|
|
482
|
+
suite: ENVELOPE_SUITE,
|
|
483
|
+
aead: ENVELOPE_AEAD,
|
|
484
|
+
x25519Epk: toHex(x25519EphKp.publicKey),
|
|
485
|
+
x25519Wrap: {
|
|
486
|
+
nonce: b64(x25519WrapNonce),
|
|
487
|
+
wrapped: b64(x25519Wrapped)
|
|
488
|
+
},
|
|
489
|
+
kyberKemCt: b64(kyberResult.ciphertext),
|
|
490
|
+
kyberWrap: {
|
|
491
|
+
nonce: b64(kyberWrapNonce),
|
|
492
|
+
wrapped: b64(kyberWrapped)
|
|
493
|
+
},
|
|
494
|
+
contentNonce: b64(contentNonce),
|
|
495
|
+
ciphertext: b64(ciphertext),
|
|
496
|
+
meta: {
|
|
497
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
498
|
+
senderName: sender?.name,
|
|
499
|
+
senderId: sender?.id
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async function hybridDecrypt(envelope, secretKeys) {
|
|
504
|
+
const version = envelope.v;
|
|
505
|
+
assertEnvelopeVersion(version);
|
|
506
|
+
let CK = null;
|
|
507
|
+
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
508
|
+
try {
|
|
509
|
+
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
510
|
+
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
511
|
+
CK = nacl.secretbox.open(
|
|
512
|
+
ub64(envelope.kyberWrap.wrapped),
|
|
513
|
+
ub64(envelope.kyberWrap.nonce),
|
|
514
|
+
kyberKek
|
|
515
|
+
);
|
|
516
|
+
if (CK) {
|
|
517
|
+
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
518
|
+
}
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
521
|
+
}
|
|
522
|
+
if (!CK) {
|
|
523
|
+
try {
|
|
524
|
+
const ephPk = fromHex(envelope.x25519Epk);
|
|
525
|
+
const sk = fromHex(secretKeys.x25519SecHex);
|
|
526
|
+
const x25519Shared = nacl.scalarMult(sk, ephPk);
|
|
527
|
+
const x25519Kek = hkdfFlex(x25519Shared, `${saltPrefix}/x25519`, "wrap-ck");
|
|
528
|
+
CK = nacl.secretbox.open(
|
|
529
|
+
ub64(envelope.x25519Wrap.wrapped),
|
|
530
|
+
ub64(envelope.x25519Wrap.nonce),
|
|
531
|
+
x25519Kek
|
|
532
|
+
);
|
|
533
|
+
if (CK) {
|
|
534
|
+
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
535
|
+
}
|
|
536
|
+
} catch (e) {
|
|
537
|
+
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (!CK) {
|
|
541
|
+
throw new Error("Could not unwrap content key with either algorithm");
|
|
542
|
+
}
|
|
543
|
+
const pt = nacl.secretbox.open(
|
|
544
|
+
ub64(envelope.ciphertext),
|
|
545
|
+
ub64(envelope.contentNonce),
|
|
546
|
+
CK
|
|
547
|
+
);
|
|
548
|
+
if (!pt) {
|
|
549
|
+
throw new Error("Content authentication failed");
|
|
550
|
+
}
|
|
551
|
+
return pt;
|
|
552
|
+
}
|
|
553
|
+
async function hybridDecryptToString(envelope, secretKeys) {
|
|
554
|
+
const pt = await hybridDecrypt(envelope, secretKeys);
|
|
555
|
+
return textDecoder.decode(pt);
|
|
556
|
+
}
|
|
557
|
+
function blake3(data, options) {
|
|
558
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
559
|
+
const opts = {};
|
|
560
|
+
if (options?.outputLength) {
|
|
561
|
+
opts.dkLen = options.outputLength;
|
|
562
|
+
}
|
|
563
|
+
if (options?.key) {
|
|
564
|
+
if (options.key.length !== 32) {
|
|
565
|
+
throw new Error("BLAKE3 key must be exactly 32 bytes");
|
|
566
|
+
}
|
|
567
|
+
opts.key = options.key;
|
|
568
|
+
}
|
|
569
|
+
if (options?.context) {
|
|
570
|
+
opts.context = new TextEncoder().encode(options.context);
|
|
571
|
+
}
|
|
572
|
+
return blake3$1(input, opts);
|
|
573
|
+
}
|
|
574
|
+
function blake3Hex(data, options) {
|
|
575
|
+
const hash = blake3(data, options);
|
|
576
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
577
|
+
}
|
|
578
|
+
function blake3Mac(key, data, outputLength = 32) {
|
|
579
|
+
if (key.length !== 32) {
|
|
580
|
+
throw new Error("BLAKE3 MAC key must be exactly 32 bytes");
|
|
581
|
+
}
|
|
582
|
+
return blake3(data, { key, outputLength });
|
|
583
|
+
}
|
|
584
|
+
function blake3DeriveKey(context, keyMaterial, outputLength = 32) {
|
|
585
|
+
return blake3(keyMaterial, { context, outputLength });
|
|
586
|
+
}
|
|
587
|
+
var BLAKE3_OUTPUT_LENGTH = 32;
|
|
588
|
+
var BLAKE3_KEY_LENGTH = 32;
|
|
589
|
+
var BLAKE3_BLOCK_SIZE = 64;
|
|
590
|
+
function chaCha20Poly1305Encrypt(key, nonce, plaintext, aad) {
|
|
591
|
+
if (key.length !== 32) {
|
|
592
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
593
|
+
}
|
|
594
|
+
if (nonce.length !== 12) {
|
|
595
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
596
|
+
}
|
|
597
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
598
|
+
return cipher.encrypt(plaintext);
|
|
599
|
+
}
|
|
600
|
+
function chaCha20Poly1305Decrypt(key, nonce, ciphertext, aad) {
|
|
601
|
+
if (key.length !== 32) {
|
|
602
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
603
|
+
}
|
|
604
|
+
if (nonce.length !== 12) {
|
|
605
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
609
|
+
return cipher.decrypt(ciphertext);
|
|
610
|
+
} catch {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function xChaCha20Poly1305Encrypt(key, nonce, plaintext, aad) {
|
|
615
|
+
if (key.length !== 32) {
|
|
616
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
617
|
+
}
|
|
618
|
+
if (nonce.length !== 24) {
|
|
619
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
620
|
+
}
|
|
621
|
+
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
622
|
+
return cipher.encrypt(plaintext);
|
|
623
|
+
}
|
|
624
|
+
function xChaCha20Poly1305Decrypt(key, nonce, ciphertext, aad) {
|
|
625
|
+
if (key.length !== 32) {
|
|
626
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
627
|
+
}
|
|
628
|
+
if (nonce.length !== 24) {
|
|
629
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
633
|
+
return cipher.decrypt(ciphertext);
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function createXChaCha20Poly1305(key, nonce, aad) {
|
|
639
|
+
if (key.length !== 32) {
|
|
640
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
641
|
+
}
|
|
642
|
+
if (nonce.length !== 24) {
|
|
643
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
644
|
+
}
|
|
645
|
+
return xchacha20poly1305(key, nonce, aad);
|
|
646
|
+
}
|
|
647
|
+
function createChaCha20Poly1305(key, nonce, aad) {
|
|
648
|
+
if (key.length !== 32) {
|
|
649
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
650
|
+
}
|
|
651
|
+
if (nonce.length !== 12) {
|
|
652
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
653
|
+
}
|
|
654
|
+
return chacha20poly1305(key, nonce, aad);
|
|
655
|
+
}
|
|
656
|
+
var CHACHA20_KEY_SIZE = 32;
|
|
657
|
+
var CHACHA20_NONCE_SIZE = 12;
|
|
658
|
+
var XCHACHA20_NONCE_SIZE = 24;
|
|
659
|
+
var POLY1305_TAG_SIZE = 16;
|
|
660
|
+
function hkdfDerive(ikm, outputLength, options) {
|
|
661
|
+
const hashFn = getHashFunction(options?.hash ?? "sha256");
|
|
662
|
+
const info = normalizeInfo(options?.info);
|
|
663
|
+
const salt = options?.salt;
|
|
664
|
+
return hkdf(hashFn, ikm, salt, info, outputLength);
|
|
665
|
+
}
|
|
666
|
+
function hkdfExtract(ikm, salt, hash = "sha256") {
|
|
667
|
+
const hashFn = getHashFunction(hash);
|
|
668
|
+
return extract(hashFn, ikm, salt);
|
|
669
|
+
}
|
|
670
|
+
function hkdfExpand(prk, info, outputLength, hash = "sha256") {
|
|
671
|
+
const hashFn = getHashFunction(hash);
|
|
672
|
+
return expand(hashFn, prk, normalizeInfo(info), outputLength);
|
|
673
|
+
}
|
|
674
|
+
function hkdfSplitForNoise(chainingKey, inputKeyMaterial) {
|
|
675
|
+
const prk = hkdfExtract(inputKeyMaterial, chainingKey, "sha256");
|
|
676
|
+
const output = hkdfExpand(prk, void 0, 64, "sha256");
|
|
677
|
+
return [
|
|
678
|
+
output.slice(0, 32),
|
|
679
|
+
// New chaining key
|
|
680
|
+
output.slice(32, 64)
|
|
681
|
+
// Output key
|
|
682
|
+
];
|
|
683
|
+
}
|
|
684
|
+
function hkdfTripleSplitForNoise(chainingKey, inputKeyMaterial) {
|
|
685
|
+
const prk = hkdfExtract(inputKeyMaterial, chainingKey, "sha256");
|
|
686
|
+
const output = hkdfExpand(prk, void 0, 96, "sha256");
|
|
687
|
+
return [
|
|
688
|
+
output.slice(0, 32),
|
|
689
|
+
// New chaining key
|
|
690
|
+
output.slice(32, 64),
|
|
691
|
+
// Output key 1
|
|
692
|
+
output.slice(64, 96)
|
|
693
|
+
// Output key 2
|
|
694
|
+
];
|
|
695
|
+
}
|
|
696
|
+
function getHashFunction(hash) {
|
|
697
|
+
switch (hash) {
|
|
698
|
+
case "sha256":
|
|
699
|
+
return sha256$1;
|
|
700
|
+
case "sha512":
|
|
701
|
+
return sha512;
|
|
702
|
+
default:
|
|
703
|
+
throw new Error(`Unsupported HKDF hash: ${hash}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function normalizeInfo(info) {
|
|
707
|
+
if (!info) return new Uint8Array(0);
|
|
708
|
+
if (typeof info === "string") return new TextEncoder().encode(info);
|
|
709
|
+
return info;
|
|
710
|
+
}
|
|
711
|
+
var HKDF_SHA256_OUTPUT_SIZE = 32;
|
|
712
|
+
var HKDF_SHA512_OUTPUT_SIZE = 64;
|
|
713
|
+
var HKDF_SHA256_MAX_OUTPUT = 255 * 32;
|
|
714
|
+
var HKDF_SHA512_MAX_OUTPUT = 255 * 64;
|
|
715
|
+
|
|
716
|
+
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, 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, 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 };
|