@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
import nacl4 from 'tweetnacl';
|
|
2
|
+
import { sha256 as sha256$1 } from '@noble/hashes/sha256';
|
|
3
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
4
|
+
import { argon2id } from 'hash-wasm';
|
|
5
|
+
import { blake3 as blake3$1 } from '@noble/hashes/blake3';
|
|
6
|
+
import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha';
|
|
7
|
+
import { hkdf, extract, expand } from '@noble/hashes/hkdf';
|
|
8
|
+
import { sha512 } from '@noble/hashes/sha512';
|
|
9
|
+
|
|
10
|
+
// src/runtime/crypto.ts
|
|
11
|
+
async function ensureCrypto() {
|
|
12
|
+
if (typeof globalThis.crypto !== "undefined") return;
|
|
13
|
+
const mod = await import('crypto');
|
|
14
|
+
const webcrypto = mod.webcrypto ?? mod.default?.webcrypto;
|
|
15
|
+
if (!webcrypto) throw new Error("WebCrypto not available in this Node runtime");
|
|
16
|
+
globalThis.crypto = webcrypto;
|
|
17
|
+
}
|
|
18
|
+
void ensureCrypto();
|
|
19
|
+
var textEncoder = new TextEncoder();
|
|
20
|
+
var textDecoder = new TextDecoder();
|
|
21
|
+
function toB64(bytes) {
|
|
22
|
+
let binary = "";
|
|
23
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
24
|
+
binary += String.fromCharCode(bytes[i]);
|
|
25
|
+
}
|
|
26
|
+
return btoa(binary);
|
|
27
|
+
}
|
|
28
|
+
function fromB64(str) {
|
|
29
|
+
const binary = atob(str);
|
|
30
|
+
const bytes = new Uint8Array(binary.length);
|
|
31
|
+
for (let i = 0; i < binary.length; i++) {
|
|
32
|
+
bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
}
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
function toHex(bytes) {
|
|
37
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
38
|
+
}
|
|
39
|
+
function fromHex(hex) {
|
|
40
|
+
const s = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
41
|
+
const normalized = s.length % 2 ? "0" + s : s;
|
|
42
|
+
const out = new Uint8Array(normalized.length / 2);
|
|
43
|
+
for (let i = 0; i < out.length; i++) {
|
|
44
|
+
out[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function assertLen(label, arr, n) {
|
|
49
|
+
if (arr.length !== n) {
|
|
50
|
+
throw new Error(`${label} must be ${n} bytes, got ${arr.length}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function rand32() {
|
|
54
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
55
|
+
}
|
|
56
|
+
function rand24() {
|
|
57
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(24));
|
|
58
|
+
}
|
|
59
|
+
function rand12() {
|
|
60
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
61
|
+
}
|
|
62
|
+
function randN(n) {
|
|
63
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
64
|
+
}
|
|
65
|
+
function sha256(bytes) {
|
|
66
|
+
return sha256$1(bytes);
|
|
67
|
+
}
|
|
68
|
+
function sha256String(str) {
|
|
69
|
+
return sha256(textEncoder.encode(str));
|
|
70
|
+
}
|
|
71
|
+
function hkdfSha256(ikm, opts) {
|
|
72
|
+
const salt = opts?.salt ?? new Uint8Array(32);
|
|
73
|
+
const info = opts?.info ?? new Uint8Array(0);
|
|
74
|
+
const L = opts?.length ?? 32;
|
|
75
|
+
const prk = hmac(sha256$1, salt, ikm);
|
|
76
|
+
let t = new Uint8Array(0);
|
|
77
|
+
const chunks = [];
|
|
78
|
+
for (let i = 1; i <= Math.ceil(L / 32); i++) {
|
|
79
|
+
const input = new Uint8Array(t.length + info.length + 1);
|
|
80
|
+
input.set(t, 0);
|
|
81
|
+
input.set(info, t.length);
|
|
82
|
+
input[input.length - 1] = i;
|
|
83
|
+
t = new Uint8Array(hmac(sha256$1, prk, input));
|
|
84
|
+
chunks.push(t);
|
|
85
|
+
}
|
|
86
|
+
const out = new Uint8Array(L);
|
|
87
|
+
let off = 0;
|
|
88
|
+
for (const c of chunks) {
|
|
89
|
+
out.set(c.subarray(0, L - off), off);
|
|
90
|
+
off += c.length;
|
|
91
|
+
if (off >= L) break;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
var b64 = toB64;
|
|
96
|
+
var ub64 = fromB64;
|
|
97
|
+
var u8 = (s) => typeof s === "string" ? textEncoder.encode(s) : s;
|
|
98
|
+
function generateX25519Keypair() {
|
|
99
|
+
const kp = nacl4.box.keyPair();
|
|
100
|
+
return {
|
|
101
|
+
publicHex: "0x" + toHex(kp.publicKey),
|
|
102
|
+
secretHex: "0x" + toHex(kp.secretKey),
|
|
103
|
+
publicBytes: kp.publicKey,
|
|
104
|
+
secretBytes: kp.secretKey
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function generateX25519KeypairFromSeed(seed) {
|
|
108
|
+
if (seed.length !== 32) {
|
|
109
|
+
throw new Error("X25519 seed must be 32 bytes");
|
|
110
|
+
}
|
|
111
|
+
const uniformSeed = sha256(seed);
|
|
112
|
+
const kp = nacl4.box.keyPair.fromSecretKey(uniformSeed);
|
|
113
|
+
return {
|
|
114
|
+
publicKey: kp.publicKey,
|
|
115
|
+
secretKey: uniformSeed
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function boxWrapWithX25519(symKey32, recipientPubKeyHex) {
|
|
119
|
+
assertLen("sym key", symKey32, 32);
|
|
120
|
+
const pk = fromHex(recipientPubKeyHex);
|
|
121
|
+
assertLen("recipient pubKey", pk, 32);
|
|
122
|
+
const eph = nacl4.box.keyPair();
|
|
123
|
+
const nonce = rand24();
|
|
124
|
+
const boxed = nacl4.box(symKey32, nonce, pk, eph.secretKey);
|
|
125
|
+
return {
|
|
126
|
+
ephPubKey: toHex(eph.publicKey),
|
|
127
|
+
nonce: b64(nonce),
|
|
128
|
+
boxed: b64(boxed)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function boxUnwrapWithX25519(wrap, recipientSecretKey32) {
|
|
132
|
+
assertLen("recipient secretKey", recipientSecretKey32, 32);
|
|
133
|
+
const ephPk = fromHex(wrap.ephPubKey);
|
|
134
|
+
assertLen("ephemeral pubKey", ephPk, 32);
|
|
135
|
+
return nacl4.box.open(
|
|
136
|
+
ub64(wrap.boxed),
|
|
137
|
+
ub64(wrap.nonce),
|
|
138
|
+
ephPk,
|
|
139
|
+
recipientSecretKey32
|
|
140
|
+
) || null;
|
|
141
|
+
}
|
|
142
|
+
function x25519SharedSecret(ourSecretKey, theirPublicKey) {
|
|
143
|
+
assertLen("secret key", ourSecretKey, 32);
|
|
144
|
+
assertLen("public key", theirPublicKey, 32);
|
|
145
|
+
return nacl4.scalarMult(ourSecretKey, theirPublicKey);
|
|
146
|
+
}
|
|
147
|
+
function deriveKeyFromShared(shared, salt, info) {
|
|
148
|
+
return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
|
|
149
|
+
}
|
|
150
|
+
var kyberModule = null;
|
|
151
|
+
async function loadKyber() {
|
|
152
|
+
if (kyberModule) return kyberModule;
|
|
153
|
+
try {
|
|
154
|
+
const m = await import('kyber-crystals');
|
|
155
|
+
const k = m.default ?? m;
|
|
156
|
+
kyberModule = k.kyber ?? k;
|
|
157
|
+
return kyberModule;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.warn("[Kyber] Failed to load kyber-crystals:", e);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function isKyberAvailable() {
|
|
164
|
+
const mod = await loadKyber();
|
|
165
|
+
return mod !== null;
|
|
166
|
+
}
|
|
167
|
+
async function generateKyberKeypair() {
|
|
168
|
+
try {
|
|
169
|
+
const mod = await loadKyber();
|
|
170
|
+
if (!mod) return null;
|
|
171
|
+
if (mod?.ready?.then) {
|
|
172
|
+
await mod.ready;
|
|
173
|
+
}
|
|
174
|
+
const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
|
|
175
|
+
if (typeof fn !== "function") {
|
|
176
|
+
console.warn("[Kyber] No keypair function found");
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const kp = await fn.call(mod);
|
|
180
|
+
const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
|
|
181
|
+
const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
|
|
182
|
+
if (!pub || !priv) {
|
|
183
|
+
console.warn("[Kyber] Invalid keypair result");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
publicB64: b64(new Uint8Array(pub)),
|
|
188
|
+
secretB64: b64(new Uint8Array(priv))
|
|
189
|
+
};
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.warn("[Kyber] Key generation failed:", e);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function kyberEncapsulate(pubKeyB64) {
|
|
196
|
+
const kyber = await loadKyber();
|
|
197
|
+
if (!kyber?.encrypt) {
|
|
198
|
+
throw new Error("Kyber encrypt not available");
|
|
199
|
+
}
|
|
200
|
+
const pk = ub64(pubKeyB64);
|
|
201
|
+
const r = await kyber.encrypt(pk);
|
|
202
|
+
const ctRaw = r?.ciphertext ?? r?.cyphertext ?? r?.ct ?? r?.bytes?.ciphertext ?? r?.bytes?.cyphertext ?? r?.bytes?.ct ?? (Array.isArray(r) ? r[0] : void 0);
|
|
203
|
+
const ssRaw = r?.key ?? r?.sharedSecret ?? r?.secret ?? r?.bytes?.key ?? r?.bytes?.sharedSecret ?? (Array.isArray(r) ? r[1] : void 0);
|
|
204
|
+
if (!ctRaw || !ssRaw) {
|
|
205
|
+
throw new Error("Kyber encapsulate failed: missing ciphertext or shared secret");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
ciphertext: new Uint8Array(ctRaw),
|
|
209
|
+
sharedSecret: new Uint8Array(ssRaw)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function kyberDecapsulate(kemCiphertextB64, secretKeyB64) {
|
|
213
|
+
const kyber = await loadKyber();
|
|
214
|
+
if (!kyber?.decrypt && !kyber?.decapsulate) {
|
|
215
|
+
throw new Error("Kyber decrypt/decapsulate not available");
|
|
216
|
+
}
|
|
217
|
+
const ct = ub64(kemCiphertextB64);
|
|
218
|
+
const sk = ub64(secretKeyB64);
|
|
219
|
+
const r = kyber.decrypt ? await kyber.decrypt(ct, sk) : await kyber.decapsulate(ct, sk);
|
|
220
|
+
const key = r && (r.key ?? r.sharedSecret) ? r.key ?? r.sharedSecret : r;
|
|
221
|
+
return new Uint8Array(key);
|
|
222
|
+
}
|
|
223
|
+
function kyberWrapKey(sharedSecret, msgKey32) {
|
|
224
|
+
if (msgKey32.length !== 32) {
|
|
225
|
+
throw new Error("Message key must be 32 bytes");
|
|
226
|
+
}
|
|
227
|
+
const kek = sha256(sharedSecret);
|
|
228
|
+
const nonce = globalThis.crypto.getRandomValues(new Uint8Array(24));
|
|
229
|
+
const wrapped = nacl4.secretbox(msgKey32, nonce, kek);
|
|
230
|
+
return {
|
|
231
|
+
nonce: b64(nonce),
|
|
232
|
+
wrapped: b64(wrapped)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function kyberUnwrapKey(sharedSecret, nonceB64, wrappedB64) {
|
|
236
|
+
const kek = sha256(sharedSecret);
|
|
237
|
+
const nonce = ub64(nonceB64);
|
|
238
|
+
const wrapped = ub64(wrappedB64);
|
|
239
|
+
return nacl4.secretbox.open(wrapped, nonce, kek) || null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/version.ts
|
|
243
|
+
var ENVELOPE_VERSION = "omnituum.hybrid.v1";
|
|
244
|
+
var ENVELOPE_VERSION_LEGACY = "pqc-demo.hybrid.v1";
|
|
245
|
+
var VAULT_VERSION = "omnituum.vault.v1";
|
|
246
|
+
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
247
|
+
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
248
|
+
var ENVELOPE_SUITE = "x25519+kyber768";
|
|
249
|
+
var ENVELOPE_AEAD = "xsalsa20poly1305";
|
|
250
|
+
var VAULT_KDF = "PBKDF2-SHA256";
|
|
251
|
+
var VAULT_KDF_V2 = "Argon2id";
|
|
252
|
+
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
253
|
+
var SUPPORTED_ENVELOPE_VERSIONS = [
|
|
254
|
+
ENVELOPE_VERSION,
|
|
255
|
+
ENVELOPE_VERSION_LEGACY
|
|
256
|
+
];
|
|
257
|
+
var SUPPORTED_VAULT_VERSIONS = [
|
|
258
|
+
VAULT_VERSION
|
|
259
|
+
];
|
|
260
|
+
var SUPPORTED_VAULT_ENCRYPTED_VERSIONS = [
|
|
261
|
+
VAULT_ENCRYPTED_VERSION,
|
|
262
|
+
VAULT_ENCRYPTED_VERSION_V2
|
|
263
|
+
];
|
|
264
|
+
var VersionMismatchError = class extends Error {
|
|
265
|
+
constructor(type, expected, received) {
|
|
266
|
+
super(
|
|
267
|
+
`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.`
|
|
268
|
+
);
|
|
269
|
+
this.type = type;
|
|
270
|
+
this.expected = expected;
|
|
271
|
+
this.received = received;
|
|
272
|
+
this.name = "VersionMismatchError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
function assertEnvelopeVersion(version) {
|
|
276
|
+
if (!SUPPORTED_ENVELOPE_VERSIONS.includes(version)) {
|
|
277
|
+
throw new VersionMismatchError("envelope", SUPPORTED_ENVELOPE_VERSIONS, version);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function assertVaultVersion(version) {
|
|
281
|
+
if (!SUPPORTED_VAULT_VERSIONS.includes(version)) {
|
|
282
|
+
throw new VersionMismatchError("vault", SUPPORTED_VAULT_VERSIONS, version);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function assertVaultEncryptedVersion(version) {
|
|
286
|
+
if (!SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version)) {
|
|
287
|
+
throw new VersionMismatchError("vault_encrypted", SUPPORTED_VAULT_ENCRYPTED_VERSIONS, version);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function isEnvelopeVersionSupported(version) {
|
|
291
|
+
return SUPPORTED_ENVELOPE_VERSIONS.includes(version);
|
|
292
|
+
}
|
|
293
|
+
function isVaultVersionSupported(version) {
|
|
294
|
+
return SUPPORTED_VAULT_VERSIONS.includes(version);
|
|
295
|
+
}
|
|
296
|
+
function isVaultEncryptedVersionSupported(version) {
|
|
297
|
+
return SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version);
|
|
298
|
+
}
|
|
299
|
+
function validateEnvelope(envelope) {
|
|
300
|
+
const errors = [];
|
|
301
|
+
if (!envelope || typeof envelope !== "object") {
|
|
302
|
+
return { valid: false, errors: ["Envelope must be an object"] };
|
|
303
|
+
}
|
|
304
|
+
const env = envelope;
|
|
305
|
+
if (typeof env.v !== "string") {
|
|
306
|
+
errors.push('Missing or invalid version field "v"');
|
|
307
|
+
} else if (!isEnvelopeVersionSupported(env.v)) {
|
|
308
|
+
errors.push(`Unsupported envelope version: ${env.v}`);
|
|
309
|
+
}
|
|
310
|
+
if (env.suite !== ENVELOPE_SUITE) {
|
|
311
|
+
errors.push(`Invalid suite: expected "${ENVELOPE_SUITE}", got "${env.suite}"`);
|
|
312
|
+
}
|
|
313
|
+
if (env.aead !== ENVELOPE_AEAD) {
|
|
314
|
+
errors.push(`Invalid aead: expected "${ENVELOPE_AEAD}", got "${env.aead}"`);
|
|
315
|
+
}
|
|
316
|
+
const required = ["x25519Epk", "x25519Wrap", "kyberKemCt", "kyberWrap", "contentNonce", "ciphertext", "meta"];
|
|
317
|
+
for (const field of required) {
|
|
318
|
+
if (!(field in env)) {
|
|
319
|
+
errors.push(`Missing required field: ${field}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
valid: errors.length === 0,
|
|
324
|
+
version: typeof env.v === "string" ? env.v : void 0,
|
|
325
|
+
errors
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function validateVault(vault) {
|
|
329
|
+
const errors = [];
|
|
330
|
+
if (!vault || typeof vault !== "object") {
|
|
331
|
+
return { valid: false, errors: ["Vault must be an object"] };
|
|
332
|
+
}
|
|
333
|
+
const v = vault;
|
|
334
|
+
if (typeof v.version !== "string") {
|
|
335
|
+
errors.push("Missing or invalid version field");
|
|
336
|
+
} else if (!isVaultVersionSupported(v.version)) {
|
|
337
|
+
errors.push(`Unsupported vault version: ${v.version}`);
|
|
338
|
+
}
|
|
339
|
+
if (!Array.isArray(v.identities)) {
|
|
340
|
+
errors.push("Missing or invalid identities array");
|
|
341
|
+
}
|
|
342
|
+
if (!v.settings || typeof v.settings !== "object") {
|
|
343
|
+
errors.push("Missing or invalid settings object");
|
|
344
|
+
}
|
|
345
|
+
if (typeof v.integrityHash !== "string") {
|
|
346
|
+
errors.push("Missing or invalid integrityHash");
|
|
347
|
+
}
|
|
348
|
+
if (typeof v.createdAt !== "string") {
|
|
349
|
+
errors.push("Missing or invalid createdAt timestamp");
|
|
350
|
+
}
|
|
351
|
+
if (typeof v.modifiedAt !== "string") {
|
|
352
|
+
errors.push("Missing or invalid modifiedAt timestamp");
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
valid: errors.length === 0,
|
|
356
|
+
version: typeof v.version === "string" ? v.version : void 0,
|
|
357
|
+
errors
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function validateEncryptedVault(encVault) {
|
|
361
|
+
const errors = [];
|
|
362
|
+
if (!encVault || typeof encVault !== "object") {
|
|
363
|
+
return { valid: false, errors: ["Encrypted vault must be an object"] };
|
|
364
|
+
}
|
|
365
|
+
const v = encVault;
|
|
366
|
+
if (typeof v.version !== "string") {
|
|
367
|
+
errors.push("Missing or invalid version field");
|
|
368
|
+
} else if (!isVaultEncryptedVersionSupported(v.version)) {
|
|
369
|
+
errors.push(`Unsupported encrypted vault version: ${v.version}`);
|
|
370
|
+
}
|
|
371
|
+
if (v.version === VAULT_ENCRYPTED_VERSION && v.kdf !== VAULT_KDF) {
|
|
372
|
+
errors.push(`Invalid kdf for v1: expected "${VAULT_KDF}", got "${v.kdf}"`);
|
|
373
|
+
} else if (v.version === VAULT_ENCRYPTED_VERSION_V2 && v.kdf !== VAULT_KDF_V2) {
|
|
374
|
+
errors.push(`Invalid kdf for v2: expected "${VAULT_KDF_V2}", got "${v.kdf}"`);
|
|
375
|
+
} else if (v.kdf !== VAULT_KDF && v.kdf !== VAULT_KDF_V2) {
|
|
376
|
+
errors.push(`Unsupported kdf: ${v.kdf}`);
|
|
377
|
+
}
|
|
378
|
+
if (v.algorithm !== VAULT_ALGORITHM) {
|
|
379
|
+
errors.push(`Invalid algorithm: expected "${VAULT_ALGORITHM}", got "${v.algorithm}"`);
|
|
380
|
+
}
|
|
381
|
+
if (typeof v.iterations !== "number" || v.iterations < 1) {
|
|
382
|
+
errors.push("Missing or invalid iterations");
|
|
383
|
+
}
|
|
384
|
+
if (typeof v.salt !== "string") {
|
|
385
|
+
errors.push("Missing or invalid salt");
|
|
386
|
+
}
|
|
387
|
+
if (typeof v.iv !== "string") {
|
|
388
|
+
errors.push("Missing or invalid iv");
|
|
389
|
+
}
|
|
390
|
+
if (typeof v.ciphertext !== "string") {
|
|
391
|
+
errors.push("Missing or invalid ciphertext");
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
valid: errors.length === 0,
|
|
395
|
+
version: typeof v.version === "string" ? v.version : void 0,
|
|
396
|
+
errors
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/crypto/hybrid.ts
|
|
401
|
+
function hkdfFlex(ikm, salt, info) {
|
|
402
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
403
|
+
}
|
|
404
|
+
function generateId() {
|
|
405
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
406
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
407
|
+
}
|
|
408
|
+
async function generateHybridIdentity(name) {
|
|
409
|
+
const x25519 = generateX25519Keypair();
|
|
410
|
+
const kyber = await generateKyberKeypair();
|
|
411
|
+
if (!kyber) {
|
|
412
|
+
console.error("Kyber key generation failed - library not available");
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
id: generateId(),
|
|
417
|
+
name,
|
|
418
|
+
x25519PubHex: x25519.publicHex,
|
|
419
|
+
x25519SecHex: x25519.secretHex,
|
|
420
|
+
kyberPubB64: kyber.publicB64,
|
|
421
|
+
kyberSecB64: kyber.secretB64,
|
|
422
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
423
|
+
rotationCount: 0
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function getPublicKeys(identity) {
|
|
427
|
+
return {
|
|
428
|
+
x25519PubHex: identity.x25519PubHex,
|
|
429
|
+
kyberPubB64: identity.kyberPubB64
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function getSecretKeys(identity) {
|
|
433
|
+
return {
|
|
434
|
+
x25519SecHex: identity.x25519SecHex,
|
|
435
|
+
kyberSecB64: identity.kyberSecB64
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function hybridEncrypt(plaintext, recipientPublicKeys, sender) {
|
|
439
|
+
const pt = typeof plaintext === "string" ? textEncoder.encode(plaintext) : plaintext;
|
|
440
|
+
const CK = rand32();
|
|
441
|
+
const contentNonce = rand24();
|
|
442
|
+
const ciphertext = nacl4.secretbox(pt, contentNonce, CK);
|
|
443
|
+
const x25519EphKp = nacl4.box.keyPair();
|
|
444
|
+
const recipientX25519Pk = fromHex(recipientPublicKeys.x25519PubHex);
|
|
445
|
+
const x25519Shared = nacl4.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
446
|
+
const x25519Kek = hkdfFlex(x25519Shared, "omnituum/x25519", "wrap-ck");
|
|
447
|
+
const x25519WrapNonce = rand24();
|
|
448
|
+
const x25519Wrapped = nacl4.secretbox(CK, x25519WrapNonce, x25519Kek);
|
|
449
|
+
const kyberResult = await kyberEncapsulate(recipientPublicKeys.kyberPubB64);
|
|
450
|
+
const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/kyber", "wrap-ck");
|
|
451
|
+
const kyberWrapNonce = rand24();
|
|
452
|
+
const kyberWrapped = nacl4.secretbox(CK, kyberWrapNonce, kyberKek);
|
|
453
|
+
return {
|
|
454
|
+
v: ENVELOPE_VERSION,
|
|
455
|
+
suite: ENVELOPE_SUITE,
|
|
456
|
+
aead: ENVELOPE_AEAD,
|
|
457
|
+
x25519Epk: toHex(x25519EphKp.publicKey),
|
|
458
|
+
x25519Wrap: {
|
|
459
|
+
nonce: b64(x25519WrapNonce),
|
|
460
|
+
wrapped: b64(x25519Wrapped)
|
|
461
|
+
},
|
|
462
|
+
kyberKemCt: b64(kyberResult.ciphertext),
|
|
463
|
+
kyberWrap: {
|
|
464
|
+
nonce: b64(kyberWrapNonce),
|
|
465
|
+
wrapped: b64(kyberWrapped)
|
|
466
|
+
},
|
|
467
|
+
contentNonce: b64(contentNonce),
|
|
468
|
+
ciphertext: b64(ciphertext),
|
|
469
|
+
meta: {
|
|
470
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
471
|
+
senderName: sender?.name,
|
|
472
|
+
senderId: sender?.id
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
async function hybridDecrypt(envelope, secretKeys) {
|
|
477
|
+
const version = envelope.v;
|
|
478
|
+
assertEnvelopeVersion(version);
|
|
479
|
+
let CK = null;
|
|
480
|
+
const saltPrefix = version === "pqc-demo.hybrid.v1" ? "pqc-demo" : "omnituum";
|
|
481
|
+
try {
|
|
482
|
+
const kyberShared = await kyberDecapsulate(envelope.kyberKemCt, secretKeys.kyberSecB64);
|
|
483
|
+
const kyberKek = hkdfFlex(kyberShared, `${saltPrefix}/kyber`, "wrap-ck");
|
|
484
|
+
CK = nacl4.secretbox.open(
|
|
485
|
+
ub64(envelope.kyberWrap.wrapped),
|
|
486
|
+
ub64(envelope.kyberWrap.nonce),
|
|
487
|
+
kyberKek
|
|
488
|
+
);
|
|
489
|
+
if (CK) {
|
|
490
|
+
console.log("[Hybrid] Decrypted using Kyber (post-quantum)");
|
|
491
|
+
}
|
|
492
|
+
} catch (e) {
|
|
493
|
+
console.warn("[Hybrid] Kyber decapsulation failed:", e);
|
|
494
|
+
}
|
|
495
|
+
if (!CK) {
|
|
496
|
+
try {
|
|
497
|
+
const ephPk = fromHex(envelope.x25519Epk);
|
|
498
|
+
const sk = fromHex(secretKeys.x25519SecHex);
|
|
499
|
+
const x25519Shared = nacl4.scalarMult(sk, ephPk);
|
|
500
|
+
const x25519Kek = hkdfFlex(x25519Shared, `${saltPrefix}/x25519`, "wrap-ck");
|
|
501
|
+
CK = nacl4.secretbox.open(
|
|
502
|
+
ub64(envelope.x25519Wrap.wrapped),
|
|
503
|
+
ub64(envelope.x25519Wrap.nonce),
|
|
504
|
+
x25519Kek
|
|
505
|
+
);
|
|
506
|
+
if (CK) {
|
|
507
|
+
console.log("[Hybrid] Decrypted using X25519 (classical)");
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
console.warn("[Hybrid] X25519 decryption failed:", e);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!CK) {
|
|
514
|
+
throw new Error("Could not unwrap content key with either algorithm");
|
|
515
|
+
}
|
|
516
|
+
const pt = nacl4.secretbox.open(
|
|
517
|
+
ub64(envelope.ciphertext),
|
|
518
|
+
ub64(envelope.contentNonce),
|
|
519
|
+
CK
|
|
520
|
+
);
|
|
521
|
+
if (!pt) {
|
|
522
|
+
throw new Error("Content authentication failed");
|
|
523
|
+
}
|
|
524
|
+
return pt;
|
|
525
|
+
}
|
|
526
|
+
async function hybridDecryptToString(envelope, secretKeys) {
|
|
527
|
+
const pt = await hybridDecrypt(envelope, secretKeys);
|
|
528
|
+
return textDecoder.decode(pt);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/crypto/dilithium.ts
|
|
532
|
+
var dilithiumModule = null;
|
|
533
|
+
async function loadDilithium() {
|
|
534
|
+
if (dilithiumModule) return dilithiumModule;
|
|
535
|
+
try {
|
|
536
|
+
const { ml_dsa65 } = await import('@noble/post-quantum/ml-dsa');
|
|
537
|
+
dilithiumModule = ml_dsa65;
|
|
538
|
+
return dilithiumModule;
|
|
539
|
+
} catch (e) {
|
|
540
|
+
console.warn("[Dilithium] Failed to load @noble/post-quantum:", e);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function isDilithiumAvailable() {
|
|
545
|
+
const mod = await loadDilithium();
|
|
546
|
+
return mod !== null;
|
|
547
|
+
}
|
|
548
|
+
async function generateDilithiumKeypair() {
|
|
549
|
+
try {
|
|
550
|
+
const mod = await loadDilithium();
|
|
551
|
+
if (!mod) return null;
|
|
552
|
+
const seed = randN(32);
|
|
553
|
+
const kp = mod.keygen(seed);
|
|
554
|
+
return {
|
|
555
|
+
publicB64: toB64(kp.publicKey),
|
|
556
|
+
secretB64: toB64(kp.secretKey)
|
|
557
|
+
};
|
|
558
|
+
} catch (e) {
|
|
559
|
+
console.warn("[Dilithium] Key generation failed:", e);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function generateDilithiumKeypairFromSeed(seed) {
|
|
564
|
+
const mod = await loadDilithium();
|
|
565
|
+
if (!mod) {
|
|
566
|
+
throw new Error("Dilithium library not available");
|
|
567
|
+
}
|
|
568
|
+
if (seed.length !== 32) {
|
|
569
|
+
throw new Error("Dilithium seed must be 32 bytes");
|
|
570
|
+
}
|
|
571
|
+
const uniformSeed = sha256(seed);
|
|
572
|
+
const kp = mod.keygen(uniformSeed);
|
|
573
|
+
return {
|
|
574
|
+
publicKey: kp.publicKey,
|
|
575
|
+
secretKey: kp.secretKey
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async function dilithiumSign(message, secretKeyB64) {
|
|
579
|
+
const mod = await loadDilithium();
|
|
580
|
+
if (!mod) {
|
|
581
|
+
throw new Error("Dilithium library not available");
|
|
582
|
+
}
|
|
583
|
+
const sk = fromB64(secretKeyB64);
|
|
584
|
+
const msg = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
585
|
+
const signature = mod.sign(sk, msg);
|
|
586
|
+
return {
|
|
587
|
+
signature: toB64(signature),
|
|
588
|
+
algorithm: "ML-DSA-65"
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function dilithiumSignRaw(message, secretKey) {
|
|
592
|
+
const mod = await loadDilithium();
|
|
593
|
+
if (!mod) {
|
|
594
|
+
throw new Error("Dilithium library not available");
|
|
595
|
+
}
|
|
596
|
+
return mod.sign(secretKey, message);
|
|
597
|
+
}
|
|
598
|
+
async function dilithiumVerify(message, signatureB64, publicKeyB64) {
|
|
599
|
+
const mod = await loadDilithium();
|
|
600
|
+
if (!mod) {
|
|
601
|
+
throw new Error("Dilithium library not available");
|
|
602
|
+
}
|
|
603
|
+
const pk = fromB64(publicKeyB64);
|
|
604
|
+
const sig = fromB64(signatureB64);
|
|
605
|
+
const msg = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
606
|
+
try {
|
|
607
|
+
return mod.verify(pk, msg, sig);
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function dilithiumVerifyRaw(message, signature, publicKey) {
|
|
613
|
+
const mod = await loadDilithium();
|
|
614
|
+
if (!mod) {
|
|
615
|
+
throw new Error("Dilithium library not available");
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
return mod.verify(publicKey, message, signature);
|
|
619
|
+
} catch {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var DILITHIUM_PUBLIC_KEY_SIZE = 1952;
|
|
624
|
+
var DILITHIUM_SECRET_KEY_SIZE = 4032;
|
|
625
|
+
var DILITHIUM_SIGNATURE_SIZE = 3309;
|
|
626
|
+
var DILITHIUM_ALGORITHM = "ML-DSA-65";
|
|
627
|
+
|
|
628
|
+
// src/vault/types.ts
|
|
629
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
630
|
+
var KDF_CONFIG_PBKDF2 = {
|
|
631
|
+
algorithm: "PBKDF2-SHA256",
|
|
632
|
+
pbkdf2Iterations: 6e5,
|
|
633
|
+
// OWASP 2023
|
|
634
|
+
saltLength: 32,
|
|
635
|
+
hashLength: 32
|
|
636
|
+
};
|
|
637
|
+
var KDF_CONFIG_ARGON2ID = {
|
|
638
|
+
algorithm: "Argon2id",
|
|
639
|
+
argon2MemoryCost: 65536,
|
|
640
|
+
// 64 MB
|
|
641
|
+
argon2TimeCost: 3,
|
|
642
|
+
argon2Parallelism: 4,
|
|
643
|
+
saltLength: 32,
|
|
644
|
+
hashLength: 32
|
|
645
|
+
};
|
|
646
|
+
var KDF_CONFIG_DEFAULT = KDF_CONFIG_PBKDF2;
|
|
647
|
+
var textEncoder2 = new TextEncoder();
|
|
648
|
+
async function derivePBKDF2(password, salt, iterations, hashLength) {
|
|
649
|
+
const passwordKey = await globalThis.crypto.subtle.importKey(
|
|
650
|
+
"raw",
|
|
651
|
+
textEncoder2.encode(password),
|
|
652
|
+
"PBKDF2",
|
|
653
|
+
false,
|
|
654
|
+
["deriveBits"]
|
|
655
|
+
);
|
|
656
|
+
const saltBuffer = new ArrayBuffer(salt.length);
|
|
657
|
+
new Uint8Array(saltBuffer).set(salt);
|
|
658
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
659
|
+
{
|
|
660
|
+
name: "PBKDF2",
|
|
661
|
+
salt: saltBuffer,
|
|
662
|
+
iterations,
|
|
663
|
+
hash: "SHA-256"
|
|
664
|
+
},
|
|
665
|
+
passwordKey,
|
|
666
|
+
hashLength * 8
|
|
667
|
+
);
|
|
668
|
+
return new Uint8Array(bits);
|
|
669
|
+
}
|
|
670
|
+
async function deriveArgon2id(password, salt, memoryCost, timeCost, parallelism, hashLength) {
|
|
671
|
+
const hash = await argon2id({
|
|
672
|
+
password,
|
|
673
|
+
salt,
|
|
674
|
+
parallelism,
|
|
675
|
+
iterations: timeCost,
|
|
676
|
+
memorySize: memoryCost,
|
|
677
|
+
hashLength,
|
|
678
|
+
outputType: "binary"
|
|
679
|
+
});
|
|
680
|
+
return new Uint8Array(hash);
|
|
681
|
+
}
|
|
682
|
+
function generateSalt(length = 32) {
|
|
683
|
+
return randN(length);
|
|
684
|
+
}
|
|
685
|
+
async function kdfDeriveKey(password, salt, config = KDF_CONFIG_DEFAULT) {
|
|
686
|
+
if (salt.length !== config.saltLength) {
|
|
687
|
+
throw new Error(`Salt must be ${config.saltLength} bytes, got ${salt.length}`);
|
|
688
|
+
}
|
|
689
|
+
if (config.algorithm === "PBKDF2-SHA256") {
|
|
690
|
+
return derivePBKDF2(
|
|
691
|
+
password,
|
|
692
|
+
salt,
|
|
693
|
+
config.pbkdf2Iterations ?? 6e5,
|
|
694
|
+
config.hashLength
|
|
695
|
+
);
|
|
696
|
+
} else if (config.algorithm === "Argon2id") {
|
|
697
|
+
return deriveArgon2id(
|
|
698
|
+
password,
|
|
699
|
+
salt,
|
|
700
|
+
config.argon2MemoryCost ?? 65536,
|
|
701
|
+
config.argon2TimeCost ?? 3,
|
|
702
|
+
config.argon2Parallelism ?? 4,
|
|
703
|
+
config.hashLength
|
|
704
|
+
);
|
|
705
|
+
} else {
|
|
706
|
+
throw new Error(`Unsupported KDF algorithm: ${config.algorithm}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function configFromParams(algorithm, params) {
|
|
710
|
+
{
|
|
711
|
+
return {
|
|
712
|
+
algorithm,
|
|
713
|
+
argon2MemoryCost: params.memoryCost,
|
|
714
|
+
argon2TimeCost: params.timeCost,
|
|
715
|
+
argon2Parallelism: params.parallelism,
|
|
716
|
+
saltLength: 32,
|
|
717
|
+
hashLength: 32
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
var _argon2Available = null;
|
|
722
|
+
async function isArgon2idAvailable() {
|
|
723
|
+
if (_argon2Available !== null) {
|
|
724
|
+
return _argon2Available;
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
await argon2id({
|
|
728
|
+
password: "test",
|
|
729
|
+
salt: new Uint8Array(16),
|
|
730
|
+
parallelism: 1,
|
|
731
|
+
iterations: 1,
|
|
732
|
+
memorySize: 1024,
|
|
733
|
+
hashLength: 32,
|
|
734
|
+
outputType: "binary"
|
|
735
|
+
});
|
|
736
|
+
_argon2Available = true;
|
|
737
|
+
} catch {
|
|
738
|
+
_argon2Available = false;
|
|
739
|
+
}
|
|
740
|
+
return _argon2Available;
|
|
741
|
+
}
|
|
742
|
+
async function getRecommendedConfig() {
|
|
743
|
+
if (await isArgon2idAvailable()) {
|
|
744
|
+
return KDF_CONFIG_ARGON2ID;
|
|
745
|
+
}
|
|
746
|
+
return KDF_CONFIG_PBKDF2;
|
|
747
|
+
}
|
|
748
|
+
async function benchmarkKDF(config = KDF_CONFIG_DEFAULT) {
|
|
749
|
+
const salt = generateSalt(config.saltLength);
|
|
750
|
+
const testPassword = "benchmark-test-password";
|
|
751
|
+
const start = performance.now();
|
|
752
|
+
await kdfDeriveKey(testPassword, salt, config);
|
|
753
|
+
const end = performance.now();
|
|
754
|
+
return end - start;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/vault/encrypt.ts
|
|
758
|
+
var textEncoder3 = new TextEncoder();
|
|
759
|
+
async function deriveKey(password, salt, iterations = PBKDF2_ITERATIONS) {
|
|
760
|
+
const passwordKey = await globalThis.crypto.subtle.importKey(
|
|
761
|
+
"raw",
|
|
762
|
+
textEncoder3.encode(password),
|
|
763
|
+
"PBKDF2",
|
|
764
|
+
false,
|
|
765
|
+
["deriveBits", "deriveKey"]
|
|
766
|
+
);
|
|
767
|
+
const saltArrayBuffer = new ArrayBuffer(salt.length);
|
|
768
|
+
new Uint8Array(saltArrayBuffer).set(salt);
|
|
769
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
770
|
+
{
|
|
771
|
+
name: "PBKDF2",
|
|
772
|
+
salt: saltArrayBuffer,
|
|
773
|
+
iterations,
|
|
774
|
+
hash: "SHA-256"
|
|
775
|
+
},
|
|
776
|
+
passwordKey,
|
|
777
|
+
{ name: "AES-GCM", length: 256 },
|
|
778
|
+
false,
|
|
779
|
+
// not extractable
|
|
780
|
+
["encrypt", "decrypt"]
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
async function encryptVault(vault, password) {
|
|
784
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
785
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
786
|
+
const key = await deriveKey(password, salt);
|
|
787
|
+
const plaintext = textEncoder3.encode(JSON.stringify(vault));
|
|
788
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
789
|
+
{ name: "AES-GCM", iv },
|
|
790
|
+
key,
|
|
791
|
+
plaintext
|
|
792
|
+
);
|
|
793
|
+
return {
|
|
794
|
+
version: VAULT_ENCRYPTED_VERSION,
|
|
795
|
+
kdf: VAULT_KDF,
|
|
796
|
+
iterations: PBKDF2_ITERATIONS,
|
|
797
|
+
salt: toB64(salt),
|
|
798
|
+
iv: toB64(iv),
|
|
799
|
+
ciphertext: toB64(new Uint8Array(ciphertext)),
|
|
800
|
+
algorithm: VAULT_ALGORITHM
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
async function encryptVaultV2(vault, password) {
|
|
804
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
805
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
806
|
+
const keyBytes = await kdfDeriveKey(password, salt, KDF_CONFIG_ARGON2ID);
|
|
807
|
+
const keyBuffer = new ArrayBuffer(keyBytes.length);
|
|
808
|
+
new Uint8Array(keyBuffer).set(keyBytes);
|
|
809
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
810
|
+
"raw",
|
|
811
|
+
keyBuffer,
|
|
812
|
+
{ name: "AES-GCM", length: 256 },
|
|
813
|
+
false,
|
|
814
|
+
["encrypt"]
|
|
815
|
+
);
|
|
816
|
+
const plaintext = textEncoder3.encode(JSON.stringify(vault));
|
|
817
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
818
|
+
{ name: "AES-GCM", iv },
|
|
819
|
+
key,
|
|
820
|
+
plaintext
|
|
821
|
+
);
|
|
822
|
+
return {
|
|
823
|
+
version: VAULT_ENCRYPTED_VERSION_V2,
|
|
824
|
+
kdf: VAULT_KDF_V2,
|
|
825
|
+
memoryCost: KDF_CONFIG_ARGON2ID.argon2MemoryCost,
|
|
826
|
+
timeCost: KDF_CONFIG_ARGON2ID.argon2TimeCost,
|
|
827
|
+
parallelism: KDF_CONFIG_ARGON2ID.argon2Parallelism,
|
|
828
|
+
salt: toB64(salt),
|
|
829
|
+
iv: toB64(iv),
|
|
830
|
+
ciphertext: toB64(new Uint8Array(ciphertext)),
|
|
831
|
+
algorithm: VAULT_ALGORITHM
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/vault/decrypt.ts
|
|
836
|
+
var textDecoder2 = new TextDecoder();
|
|
837
|
+
async function decryptVault(encryptedFile, password) {
|
|
838
|
+
assertVaultEncryptedVersion(encryptedFile.version);
|
|
839
|
+
const salt = fromB64(encryptedFile.salt);
|
|
840
|
+
const iv = fromB64(encryptedFile.iv);
|
|
841
|
+
const ciphertext = fromB64(encryptedFile.ciphertext);
|
|
842
|
+
let key;
|
|
843
|
+
if (encryptedFile.version === VAULT_ENCRYPTED_VERSION_V2) {
|
|
844
|
+
const v2File = encryptedFile;
|
|
845
|
+
const kdfConfig = configFromParams("Argon2id", {
|
|
846
|
+
memoryCost: v2File.memoryCost,
|
|
847
|
+
timeCost: v2File.timeCost,
|
|
848
|
+
parallelism: v2File.parallelism
|
|
849
|
+
});
|
|
850
|
+
const keyBytes = await kdfDeriveKey(password, salt, kdfConfig);
|
|
851
|
+
const keyBuffer = new ArrayBuffer(keyBytes.length);
|
|
852
|
+
new Uint8Array(keyBuffer).set(keyBytes);
|
|
853
|
+
key = await globalThis.crypto.subtle.importKey(
|
|
854
|
+
"raw",
|
|
855
|
+
keyBuffer,
|
|
856
|
+
{ name: "AES-GCM", length: 256 },
|
|
857
|
+
false,
|
|
858
|
+
["decrypt"]
|
|
859
|
+
);
|
|
860
|
+
} else {
|
|
861
|
+
key = await deriveKey(password, salt, encryptedFile.iterations);
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
const ivArrayBuffer = new ArrayBuffer(iv.length);
|
|
865
|
+
new Uint8Array(ivArrayBuffer).set(iv);
|
|
866
|
+
const ciphertextArrayBuffer = new ArrayBuffer(ciphertext.length);
|
|
867
|
+
new Uint8Array(ciphertextArrayBuffer).set(ciphertext);
|
|
868
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
869
|
+
{ name: "AES-GCM", iv: ivArrayBuffer },
|
|
870
|
+
key,
|
|
871
|
+
ciphertextArrayBuffer
|
|
872
|
+
);
|
|
873
|
+
const json = textDecoder2.decode(plaintext);
|
|
874
|
+
const vault = JSON.parse(json);
|
|
875
|
+
assertVaultVersion(vault.version);
|
|
876
|
+
if (!Array.isArray(vault.identities)) {
|
|
877
|
+
throw new Error("Invalid vault structure: missing identities array");
|
|
878
|
+
}
|
|
879
|
+
return vault;
|
|
880
|
+
} catch (error) {
|
|
881
|
+
if (error instanceof DOMException && error.name === "OperationError") {
|
|
882
|
+
throw new Error("Incorrect password or corrupted vault");
|
|
883
|
+
}
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/utils/integrity.ts
|
|
889
|
+
function computeIntegrityHash(identities) {
|
|
890
|
+
const canonical = identities.map((i) => ({
|
|
891
|
+
id: i.id,
|
|
892
|
+
name: i.name,
|
|
893
|
+
x25519PubHex: i.x25519PubHex,
|
|
894
|
+
kyberPubB64: i.kyberPubB64,
|
|
895
|
+
createdAt: i.createdAt,
|
|
896
|
+
rotationCount: i.rotationCount
|
|
897
|
+
}));
|
|
898
|
+
const serialized = JSON.stringify(canonical, Object.keys(canonical[0] || {}).sort());
|
|
899
|
+
return computeStringHash(serialized);
|
|
900
|
+
}
|
|
901
|
+
function computeStringHash(str) {
|
|
902
|
+
let hash = 0;
|
|
903
|
+
for (let i = 0; i < str.length; i++) {
|
|
904
|
+
const char = str.charCodeAt(i);
|
|
905
|
+
hash = (hash << 5) - hash + char;
|
|
906
|
+
hash = hash & hash;
|
|
907
|
+
}
|
|
908
|
+
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
909
|
+
}
|
|
910
|
+
async function computeHashAsync(data) {
|
|
911
|
+
const encoder = new TextEncoder();
|
|
912
|
+
const bytes = encoder.encode(data);
|
|
913
|
+
const subtle = globalThis.crypto?.subtle;
|
|
914
|
+
if (subtle) {
|
|
915
|
+
const hashBuffer = await subtle.digest("SHA-256", bytes);
|
|
916
|
+
return toHex(new Uint8Array(hashBuffer));
|
|
917
|
+
}
|
|
918
|
+
const { createHash } = await import('crypto');
|
|
919
|
+
return toHex(new Uint8Array(createHash("sha256").update(bytes).digest()));
|
|
920
|
+
}
|
|
921
|
+
async function computeKeyFingerprint(identity) {
|
|
922
|
+
const combined = identity.x25519PubHex + identity.kyberPubB64;
|
|
923
|
+
const hash = await computeHashAsync(combined);
|
|
924
|
+
return hash.slice(0, 16).toUpperCase();
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/utils/entropy.ts
|
|
928
|
+
function generateId2() {
|
|
929
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
930
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
931
|
+
}
|
|
932
|
+
function secretboxEncrypt(key, plaintext, nonce) {
|
|
933
|
+
assertLen("secretbox key", key, 32);
|
|
934
|
+
const n = nonce ?? rand24();
|
|
935
|
+
assertLen("nonce", n, 24);
|
|
936
|
+
const ciphertext = nacl4.secretbox(plaintext, n, key);
|
|
937
|
+
return {
|
|
938
|
+
scheme: "nacl.secretbox",
|
|
939
|
+
nonce: toB64(n),
|
|
940
|
+
ciphertext: toB64(ciphertext)
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
function secretboxEncryptString(key, plaintext, nonce) {
|
|
944
|
+
return secretboxEncrypt(key, new TextEncoder().encode(plaintext), nonce);
|
|
945
|
+
}
|
|
946
|
+
function secretboxDecrypt(key, payload) {
|
|
947
|
+
if (payload.scheme !== "nacl.secretbox") {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
assertLen("secretbox key", key, 32);
|
|
951
|
+
const nonce = fromB64(payload.nonce);
|
|
952
|
+
const ciphertext = fromB64(payload.ciphertext);
|
|
953
|
+
return nacl4.secretbox.open(ciphertext, nonce, key) || null;
|
|
954
|
+
}
|
|
955
|
+
function secretboxDecryptString(key, payload) {
|
|
956
|
+
const result = secretboxDecrypt(key, payload);
|
|
957
|
+
if (!result) return null;
|
|
958
|
+
return new TextDecoder().decode(result);
|
|
959
|
+
}
|
|
960
|
+
function secretboxRaw(key, plaintext, nonce) {
|
|
961
|
+
assertLen("secretbox key", key, 32);
|
|
962
|
+
assertLen("nonce", nonce, 24);
|
|
963
|
+
return nacl4.secretbox(plaintext, nonce, key);
|
|
964
|
+
}
|
|
965
|
+
function secretboxOpenRaw(key, ciphertext, nonce) {
|
|
966
|
+
assertLen("secretbox key", key, 32);
|
|
967
|
+
assertLen("nonce", nonce, 24);
|
|
968
|
+
return nacl4.secretbox.open(ciphertext, nonce, key) || null;
|
|
969
|
+
}
|
|
970
|
+
function boxEncrypt(plaintext, nonce, recipientPubKey, senderSecKey) {
|
|
971
|
+
assertLen("nonce", nonce, 24);
|
|
972
|
+
assertLen("recipient pubKey", recipientPubKey, 32);
|
|
973
|
+
assertLen("sender secKey", senderSecKey, 32);
|
|
974
|
+
return nacl4.box(plaintext, nonce, recipientPubKey, senderSecKey);
|
|
975
|
+
}
|
|
976
|
+
function boxDecrypt(ciphertext, nonce, senderPubKey, recipientSecKey) {
|
|
977
|
+
assertLen("nonce", nonce, 24);
|
|
978
|
+
assertLen("sender pubKey", senderPubKey, 32);
|
|
979
|
+
assertLen("recipient secKey", recipientSecKey, 32);
|
|
980
|
+
return nacl4.box.open(ciphertext, nonce, senderPubKey, recipientSecKey) || null;
|
|
981
|
+
}
|
|
982
|
+
var SECRETBOX_KEY_SIZE = nacl4.secretbox.keyLength;
|
|
983
|
+
var SECRETBOX_NONCE_SIZE = nacl4.secretbox.nonceLength;
|
|
984
|
+
var SECRETBOX_OVERHEAD = nacl4.secretbox.overheadLength;
|
|
985
|
+
var BOX_KEY_SIZE = nacl4.box.publicKeyLength;
|
|
986
|
+
var BOX_NONCE_SIZE = nacl4.box.nonceLength;
|
|
987
|
+
function blake3(data, options) {
|
|
988
|
+
const input = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
989
|
+
const opts = {};
|
|
990
|
+
if (options?.outputLength) {
|
|
991
|
+
opts.dkLen = options.outputLength;
|
|
992
|
+
}
|
|
993
|
+
if (options?.key) {
|
|
994
|
+
if (options.key.length !== 32) {
|
|
995
|
+
throw new Error("BLAKE3 key must be exactly 32 bytes");
|
|
996
|
+
}
|
|
997
|
+
opts.key = options.key;
|
|
998
|
+
}
|
|
999
|
+
if (options?.context) {
|
|
1000
|
+
opts.context = new TextEncoder().encode(options.context);
|
|
1001
|
+
}
|
|
1002
|
+
return blake3$1(input, opts);
|
|
1003
|
+
}
|
|
1004
|
+
function blake3Hex(data, options) {
|
|
1005
|
+
const hash = blake3(data, options);
|
|
1006
|
+
return Array.from(hash).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1007
|
+
}
|
|
1008
|
+
function blake3Mac(key, data, outputLength = 32) {
|
|
1009
|
+
if (key.length !== 32) {
|
|
1010
|
+
throw new Error("BLAKE3 MAC key must be exactly 32 bytes");
|
|
1011
|
+
}
|
|
1012
|
+
return blake3(data, { key, outputLength });
|
|
1013
|
+
}
|
|
1014
|
+
function blake3DeriveKey(context, keyMaterial, outputLength = 32) {
|
|
1015
|
+
return blake3(keyMaterial, { context, outputLength });
|
|
1016
|
+
}
|
|
1017
|
+
var BLAKE3_OUTPUT_LENGTH = 32;
|
|
1018
|
+
function chaCha20Poly1305Encrypt(key, nonce, plaintext, aad) {
|
|
1019
|
+
if (key.length !== 32) {
|
|
1020
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
1021
|
+
}
|
|
1022
|
+
if (nonce.length !== 12) {
|
|
1023
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
1024
|
+
}
|
|
1025
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
1026
|
+
return cipher.encrypt(plaintext);
|
|
1027
|
+
}
|
|
1028
|
+
function chaCha20Poly1305Decrypt(key, nonce, ciphertext, aad) {
|
|
1029
|
+
if (key.length !== 32) {
|
|
1030
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
1031
|
+
}
|
|
1032
|
+
if (nonce.length !== 12) {
|
|
1033
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
1037
|
+
return cipher.decrypt(ciphertext);
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
function xChaCha20Poly1305Encrypt(key, nonce, plaintext, aad) {
|
|
1043
|
+
if (key.length !== 32) {
|
|
1044
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
1045
|
+
}
|
|
1046
|
+
if (nonce.length !== 24) {
|
|
1047
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
1048
|
+
}
|
|
1049
|
+
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
1050
|
+
return cipher.encrypt(plaintext);
|
|
1051
|
+
}
|
|
1052
|
+
function xChaCha20Poly1305Decrypt(key, nonce, ciphertext, aad) {
|
|
1053
|
+
if (key.length !== 32) {
|
|
1054
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
1055
|
+
}
|
|
1056
|
+
if (nonce.length !== 24) {
|
|
1057
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
const cipher = xchacha20poly1305(key, nonce, aad);
|
|
1061
|
+
return cipher.decrypt(ciphertext);
|
|
1062
|
+
} catch {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function createXChaCha20Poly1305(key, nonce, aad) {
|
|
1067
|
+
if (key.length !== 32) {
|
|
1068
|
+
throw new Error("XChaCha20-Poly1305 key must be 32 bytes");
|
|
1069
|
+
}
|
|
1070
|
+
if (nonce.length !== 24) {
|
|
1071
|
+
throw new Error("XChaCha20-Poly1305 nonce must be 24 bytes");
|
|
1072
|
+
}
|
|
1073
|
+
return xchacha20poly1305(key, nonce, aad);
|
|
1074
|
+
}
|
|
1075
|
+
function createChaCha20Poly1305(key, nonce, aad) {
|
|
1076
|
+
if (key.length !== 32) {
|
|
1077
|
+
throw new Error("ChaCha20-Poly1305 key must be 32 bytes");
|
|
1078
|
+
}
|
|
1079
|
+
if (nonce.length !== 12) {
|
|
1080
|
+
throw new Error("ChaCha20-Poly1305 nonce must be 12 bytes");
|
|
1081
|
+
}
|
|
1082
|
+
return chacha20poly1305(key, nonce, aad);
|
|
1083
|
+
}
|
|
1084
|
+
var CHACHA20_KEY_SIZE = 32;
|
|
1085
|
+
var XCHACHA20_NONCE_SIZE = 24;
|
|
1086
|
+
var POLY1305_TAG_SIZE = 16;
|
|
1087
|
+
function hkdfDerive(ikm, outputLength, options) {
|
|
1088
|
+
const hashFn = getHashFunction(options?.hash ?? "sha256");
|
|
1089
|
+
const info = normalizeInfo(options?.info);
|
|
1090
|
+
const salt = options?.salt;
|
|
1091
|
+
return hkdf(hashFn, ikm, salt, info, outputLength);
|
|
1092
|
+
}
|
|
1093
|
+
function hkdfExtract(ikm, salt, hash = "sha256") {
|
|
1094
|
+
const hashFn = getHashFunction(hash);
|
|
1095
|
+
return extract(hashFn, ikm, salt);
|
|
1096
|
+
}
|
|
1097
|
+
function hkdfExpand(prk, info, outputLength, hash = "sha256") {
|
|
1098
|
+
const hashFn = getHashFunction(hash);
|
|
1099
|
+
return expand(hashFn, prk, normalizeInfo(info), outputLength);
|
|
1100
|
+
}
|
|
1101
|
+
function hkdfSplitForNoise(chainingKey, inputKeyMaterial) {
|
|
1102
|
+
const prk = hkdfExtract(inputKeyMaterial, chainingKey, "sha256");
|
|
1103
|
+
const output = hkdfExpand(prk, void 0, 64, "sha256");
|
|
1104
|
+
return [
|
|
1105
|
+
output.slice(0, 32),
|
|
1106
|
+
// New chaining key
|
|
1107
|
+
output.slice(32, 64)
|
|
1108
|
+
// Output key
|
|
1109
|
+
];
|
|
1110
|
+
}
|
|
1111
|
+
function hkdfTripleSplitForNoise(chainingKey, inputKeyMaterial) {
|
|
1112
|
+
const prk = hkdfExtract(inputKeyMaterial, chainingKey, "sha256");
|
|
1113
|
+
const output = hkdfExpand(prk, void 0, 96, "sha256");
|
|
1114
|
+
return [
|
|
1115
|
+
output.slice(0, 32),
|
|
1116
|
+
// New chaining key
|
|
1117
|
+
output.slice(32, 64),
|
|
1118
|
+
// Output key 1
|
|
1119
|
+
output.slice(64, 96)
|
|
1120
|
+
// Output key 2
|
|
1121
|
+
];
|
|
1122
|
+
}
|
|
1123
|
+
function getHashFunction(hash) {
|
|
1124
|
+
switch (hash) {
|
|
1125
|
+
case "sha256":
|
|
1126
|
+
return sha256$1;
|
|
1127
|
+
case "sha512":
|
|
1128
|
+
return sha512;
|
|
1129
|
+
default:
|
|
1130
|
+
throw new Error(`Unsupported HKDF hash: ${hash}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function normalizeInfo(info) {
|
|
1134
|
+
if (!info) return new Uint8Array(0);
|
|
1135
|
+
if (typeof info === "string") return new TextEncoder().encode(info);
|
|
1136
|
+
return info;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/vault/manager.ts
|
|
1140
|
+
function createEmptyVault() {
|
|
1141
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1142
|
+
return {
|
|
1143
|
+
version: VAULT_VERSION,
|
|
1144
|
+
identities: [],
|
|
1145
|
+
settings: {
|
|
1146
|
+
autoUnlock: false,
|
|
1147
|
+
lockTimeout: 15,
|
|
1148
|
+
showFingerprints: true
|
|
1149
|
+
},
|
|
1150
|
+
integrityHash: computeIntegrityHash([]),
|
|
1151
|
+
createdAt: now,
|
|
1152
|
+
modifiedAt: now
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
async function createIdentity(name) {
|
|
1156
|
+
const x25519 = generateX25519Keypair();
|
|
1157
|
+
const kyber = await generateKyberKeypair();
|
|
1158
|
+
if (!kyber) {
|
|
1159
|
+
console.error("Kyber key generation failed");
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1163
|
+
const deviceFingerprint = await getDeviceFingerprint();
|
|
1164
|
+
return {
|
|
1165
|
+
id: generateId2(),
|
|
1166
|
+
name,
|
|
1167
|
+
x25519PubHex: x25519.publicHex,
|
|
1168
|
+
x25519SecHex: x25519.secretHex,
|
|
1169
|
+
kyberPubB64: kyber.publicB64,
|
|
1170
|
+
kyberSecB64: kyber.secretB64,
|
|
1171
|
+
createdAt: now,
|
|
1172
|
+
rotationCount: 0,
|
|
1173
|
+
deviceFingerprint
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function addIdentity(vault, identity) {
|
|
1177
|
+
const identities = [...vault.identities, identity];
|
|
1178
|
+
return {
|
|
1179
|
+
...vault,
|
|
1180
|
+
identities,
|
|
1181
|
+
integrityHash: computeIntegrityHash(identities),
|
|
1182
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async function getDeviceFingerprint() {
|
|
1186
|
+
const data = [
|
|
1187
|
+
navigator.userAgent,
|
|
1188
|
+
navigator.language,
|
|
1189
|
+
screen.width,
|
|
1190
|
+
screen.height,
|
|
1191
|
+
(/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
1192
|
+
].join("|");
|
|
1193
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
|
|
1194
|
+
return toHex(new Uint8Array(hash)).slice(0, 16);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/security/index.ts
|
|
1198
|
+
function zeroMemory(arr) {
|
|
1199
|
+
if (!arr || arr.length === 0) return;
|
|
1200
|
+
arr.fill(0);
|
|
1201
|
+
if (arr[0] !== 0) {
|
|
1202
|
+
throw new Error("Memory zeroing failed");
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function zeroAll(...arrays) {
|
|
1206
|
+
for (const arr of arrays) {
|
|
1207
|
+
if (arr) {
|
|
1208
|
+
zeroMemory(arr);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function withSecureData(getData, process) {
|
|
1213
|
+
let data = null;
|
|
1214
|
+
try {
|
|
1215
|
+
data = await getData();
|
|
1216
|
+
return await process(data);
|
|
1217
|
+
} finally {
|
|
1218
|
+
if (data) {
|
|
1219
|
+
zeroMemory(data);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
function constantTimeEqual(a, b) {
|
|
1224
|
+
if (a.length !== b.length) {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
let result = 0;
|
|
1228
|
+
for (let i = 0; i < a.length; i++) {
|
|
1229
|
+
result |= a[i] ^ b[i];
|
|
1230
|
+
}
|
|
1231
|
+
return result === 0;
|
|
1232
|
+
}
|
|
1233
|
+
function createSession(timeoutMs = 15 * 60 * 1e3) {
|
|
1234
|
+
return {
|
|
1235
|
+
unlocked: false,
|
|
1236
|
+
unlockedAt: null,
|
|
1237
|
+
timeoutMs,
|
|
1238
|
+
unlockReason: null,
|
|
1239
|
+
sessionId: null,
|
|
1240
|
+
lastActivityAt: null,
|
|
1241
|
+
failedAttempts: 0,
|
|
1242
|
+
lockedOutUntil: null
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function unlockSecureSession(session, reason = "password") {
|
|
1246
|
+
const now = Date.now();
|
|
1247
|
+
return {
|
|
1248
|
+
...session,
|
|
1249
|
+
unlocked: true,
|
|
1250
|
+
unlockedAt: now,
|
|
1251
|
+
unlockReason: reason,
|
|
1252
|
+
sessionId: generateSessionId(),
|
|
1253
|
+
lastActivityAt: now,
|
|
1254
|
+
failedAttempts: 0,
|
|
1255
|
+
lockedOutUntil: null
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function lockSecureSession(session) {
|
|
1259
|
+
return {
|
|
1260
|
+
...session,
|
|
1261
|
+
unlocked: false,
|
|
1262
|
+
unlockedAt: null,
|
|
1263
|
+
unlockReason: null,
|
|
1264
|
+
sessionId: null,
|
|
1265
|
+
lastActivityAt: null
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function isSessionTimedOut(session) {
|
|
1269
|
+
if (!session.unlocked || session.timeoutMs === 0) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
const lastActivity = session.lastActivityAt ?? session.unlockedAt ?? 0;
|
|
1273
|
+
return Date.now() - lastActivity > session.timeoutMs;
|
|
1274
|
+
}
|
|
1275
|
+
function generateSessionId() {
|
|
1276
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
1277
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1278
|
+
}
|
|
1279
|
+
var SecureBuffer = class {
|
|
1280
|
+
constructor(data) {
|
|
1281
|
+
this._disposed = false;
|
|
1282
|
+
this._data = new Uint8Array(data.length);
|
|
1283
|
+
this._data.set(data);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get a copy of the data (original stays protected).
|
|
1287
|
+
*/
|
|
1288
|
+
get data() {
|
|
1289
|
+
if (this._disposed) {
|
|
1290
|
+
throw new Error("SecureBuffer has been disposed");
|
|
1291
|
+
}
|
|
1292
|
+
const copy = new Uint8Array(this._data.length);
|
|
1293
|
+
copy.set(this._data);
|
|
1294
|
+
return copy;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Get data length without exposing contents.
|
|
1298
|
+
*/
|
|
1299
|
+
get length() {
|
|
1300
|
+
return this._data.length;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Check if buffer has been disposed.
|
|
1304
|
+
*/
|
|
1305
|
+
get isDisposed() {
|
|
1306
|
+
return this._disposed;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Zero and dispose the buffer.
|
|
1310
|
+
*/
|
|
1311
|
+
dispose() {
|
|
1312
|
+
if (!this._disposed) {
|
|
1313
|
+
zeroMemory(this._data);
|
|
1314
|
+
this._disposed = true;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Execute a function with the data, then dispose.
|
|
1319
|
+
*/
|
|
1320
|
+
async useAndDispose(fn) {
|
|
1321
|
+
try {
|
|
1322
|
+
return await fn(this._data);
|
|
1323
|
+
} finally {
|
|
1324
|
+
this.dispose();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// src/vault/migrate.ts
|
|
1330
|
+
function needsMigration(encryptedVault) {
|
|
1331
|
+
return encryptedVault.version === VAULT_ENCRYPTED_VERSION;
|
|
1332
|
+
}
|
|
1333
|
+
function isV2Vault(encryptedVault) {
|
|
1334
|
+
return encryptedVault.version === VAULT_ENCRYPTED_VERSION_V2;
|
|
1335
|
+
}
|
|
1336
|
+
function getVaultKdfInfo(encryptedVault) {
|
|
1337
|
+
if (encryptedVault.version === VAULT_ENCRYPTED_VERSION_V2) {
|
|
1338
|
+
const v2 = encryptedVault;
|
|
1339
|
+
return {
|
|
1340
|
+
kdf: `Argon2id (${v2.memoryCost / 1024}MB, ${v2.timeCost} iterations)`,
|
|
1341
|
+
version: "v2",
|
|
1342
|
+
isSecure: true
|
|
1343
|
+
};
|
|
1344
|
+
} else {
|
|
1345
|
+
const v1 = encryptedVault;
|
|
1346
|
+
return {
|
|
1347
|
+
kdf: `PBKDF2-SHA256 (${v1.iterations.toLocaleString()} iterations)`,
|
|
1348
|
+
version: "v1",
|
|
1349
|
+
isSecure: true,
|
|
1350
|
+
// Still secure, just not as modern
|
|
1351
|
+
recommendation: "Consider upgrading to Argon2id for stronger protection"
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
async function migrateEncryptedVault(options) {
|
|
1356
|
+
const { encryptedVault, password, keepBackup = false } = options;
|
|
1357
|
+
if (isV2Vault(encryptedVault)) {
|
|
1358
|
+
throw new Error("Vault is already v2 format, no migration needed");
|
|
1359
|
+
}
|
|
1360
|
+
let decryptedVaultJson = null;
|
|
1361
|
+
try {
|
|
1362
|
+
const vault = await decryptVault(encryptedVault, password);
|
|
1363
|
+
const vaultJson = JSON.stringify(vault);
|
|
1364
|
+
decryptedVaultJson = new TextEncoder().encode(vaultJson);
|
|
1365
|
+
const newEncryptedVault = await encryptVaultV2(vault, password);
|
|
1366
|
+
return {
|
|
1367
|
+
encryptedVault: newEncryptedVault,
|
|
1368
|
+
backup: keepBackup ? encryptedVault : void 0,
|
|
1369
|
+
sourceVersion: encryptedVault.version,
|
|
1370
|
+
targetVersion: VAULT_ENCRYPTED_VERSION_V2,
|
|
1371
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1372
|
+
};
|
|
1373
|
+
} finally {
|
|
1374
|
+
if (decryptedVaultJson) {
|
|
1375
|
+
zeroMemory(decryptedVaultJson);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/fs/types.ts
|
|
1381
|
+
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
1382
|
+
var OQE_FORMAT_VERSION = 1;
|
|
1383
|
+
var ALGORITHM_SUITES = {
|
|
1384
|
+
/** Hybrid: X25519 ECDH + Kyber768 KEM + AES-256-GCM */
|
|
1385
|
+
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
1386
|
+
/** Password: Argon2id + AES-256-GCM */
|
|
1387
|
+
PASSWORD_ARGON2ID_AES256GCM: 2
|
|
1388
|
+
};
|
|
1389
|
+
var DEFAULT_ARGON2ID_PARAMS = {
|
|
1390
|
+
memoryCost: 65536,
|
|
1391
|
+
// 64 MB
|
|
1392
|
+
timeCost: 3,
|
|
1393
|
+
parallelism: 4,
|
|
1394
|
+
hashLength: 32,
|
|
1395
|
+
saltLength: 32
|
|
1396
|
+
};
|
|
1397
|
+
var OQE_HEADER_SIZE = 30;
|
|
1398
|
+
var OQEError = class extends Error {
|
|
1399
|
+
constructor(code, message) {
|
|
1400
|
+
super(message);
|
|
1401
|
+
this.code = code;
|
|
1402
|
+
this.name = "OQEError";
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
async function toUint8Array(input) {
|
|
1406
|
+
if (input instanceof Uint8Array) {
|
|
1407
|
+
return input;
|
|
1408
|
+
}
|
|
1409
|
+
if (input instanceof ArrayBuffer) {
|
|
1410
|
+
return new Uint8Array(input);
|
|
1411
|
+
}
|
|
1412
|
+
const buffer = await input.arrayBuffer();
|
|
1413
|
+
return new Uint8Array(buffer);
|
|
1414
|
+
}
|
|
1415
|
+
async function deriveKeyFromPassword(password, salt, params = DEFAULT_ARGON2ID_PARAMS) {
|
|
1416
|
+
if (salt.length !== params.saltLength) {
|
|
1417
|
+
throw new Error(`Salt must be ${params.saltLength} bytes, got ${salt.length}`);
|
|
1418
|
+
}
|
|
1419
|
+
const hash = await argon2id({
|
|
1420
|
+
password,
|
|
1421
|
+
salt,
|
|
1422
|
+
parallelism: params.parallelism,
|
|
1423
|
+
iterations: params.timeCost,
|
|
1424
|
+
memorySize: params.memoryCost,
|
|
1425
|
+
hashLength: params.hashLength,
|
|
1426
|
+
outputType: "binary"
|
|
1427
|
+
});
|
|
1428
|
+
return new Uint8Array(hash);
|
|
1429
|
+
}
|
|
1430
|
+
function generateArgon2Salt(length = 32) {
|
|
1431
|
+
return randN(length);
|
|
1432
|
+
}
|
|
1433
|
+
var _argon2Available2 = null;
|
|
1434
|
+
async function isArgon2Available() {
|
|
1435
|
+
if (_argon2Available2 !== null) {
|
|
1436
|
+
return _argon2Available2;
|
|
1437
|
+
}
|
|
1438
|
+
try {
|
|
1439
|
+
await argon2id({
|
|
1440
|
+
password: "test",
|
|
1441
|
+
salt: new Uint8Array(16),
|
|
1442
|
+
parallelism: 1,
|
|
1443
|
+
iterations: 1,
|
|
1444
|
+
memorySize: 1024,
|
|
1445
|
+
// 1 MB
|
|
1446
|
+
hashLength: 32,
|
|
1447
|
+
outputType: "binary"
|
|
1448
|
+
});
|
|
1449
|
+
_argon2Available2 = true;
|
|
1450
|
+
} catch {
|
|
1451
|
+
_argon2Available2 = false;
|
|
1452
|
+
}
|
|
1453
|
+
return _argon2Available2;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/fs/aes.ts
|
|
1457
|
+
function toArrayBuffer(arr) {
|
|
1458
|
+
if (arr.buffer instanceof SharedArrayBuffer || arr.byteOffset !== 0 || arr.byteLength !== arr.buffer.byteLength) {
|
|
1459
|
+
const copy = new ArrayBuffer(arr.byteLength);
|
|
1460
|
+
new Uint8Array(copy).set(arr);
|
|
1461
|
+
return copy;
|
|
1462
|
+
}
|
|
1463
|
+
return arr.buffer;
|
|
1464
|
+
}
|
|
1465
|
+
var AES_KEY_SIZE = 32;
|
|
1466
|
+
var AES_GCM_IV_SIZE = 12;
|
|
1467
|
+
async function importAesKey(keyBytes) {
|
|
1468
|
+
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
1469
|
+
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
1470
|
+
}
|
|
1471
|
+
return globalThis.crypto.subtle.importKey(
|
|
1472
|
+
"raw",
|
|
1473
|
+
toArrayBuffer(keyBytes),
|
|
1474
|
+
{ name: "AES-GCM", length: 256 },
|
|
1475
|
+
false,
|
|
1476
|
+
// not extractable
|
|
1477
|
+
["encrypt", "decrypt"]
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
1481
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
1482
|
+
const ivBytes = iv ?? rand12();
|
|
1483
|
+
if (ivBytes.length !== AES_GCM_IV_SIZE) {
|
|
1484
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${ivBytes.length}`);
|
|
1485
|
+
}
|
|
1486
|
+
const encrypted = await globalThis.crypto.subtle.encrypt(
|
|
1487
|
+
{
|
|
1488
|
+
name: "AES-GCM",
|
|
1489
|
+
iv: toArrayBuffer(ivBytes),
|
|
1490
|
+
additionalData: void 0,
|
|
1491
|
+
tagLength: 128
|
|
1492
|
+
// 16 bytes
|
|
1493
|
+
},
|
|
1494
|
+
cryptoKey,
|
|
1495
|
+
toArrayBuffer(plaintext)
|
|
1496
|
+
);
|
|
1497
|
+
return {
|
|
1498
|
+
iv: ivBytes,
|
|
1499
|
+
ciphertext: new Uint8Array(encrypted)
|
|
1500
|
+
// Includes auth tag
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
1504
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
1505
|
+
if (iv.length !== AES_GCM_IV_SIZE) {
|
|
1506
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${iv.length}`);
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
const decrypted = await globalThis.crypto.subtle.decrypt(
|
|
1510
|
+
{
|
|
1511
|
+
name: "AES-GCM",
|
|
1512
|
+
iv: toArrayBuffer(iv),
|
|
1513
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
1514
|
+
tagLength: 128
|
|
1515
|
+
},
|
|
1516
|
+
cryptoKey,
|
|
1517
|
+
toArrayBuffer(ciphertext)
|
|
1518
|
+
);
|
|
1519
|
+
return new Uint8Array(decrypted);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
throw new Error("Decryption failed: authentication tag mismatch (wrong key or corrupted data)");
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/fs/format.ts
|
|
1526
|
+
function writeUint32BE(value) {
|
|
1527
|
+
const buffer = new ArrayBuffer(4);
|
|
1528
|
+
new DataView(buffer).setUint32(0, value, false);
|
|
1529
|
+
return new Uint8Array(buffer);
|
|
1530
|
+
}
|
|
1531
|
+
function readUint32BE(data, offset) {
|
|
1532
|
+
return new DataView(data.buffer, data.byteOffset + offset).getUint32(0, false);
|
|
1533
|
+
}
|
|
1534
|
+
function writeUint16BE(value) {
|
|
1535
|
+
const buffer = new ArrayBuffer(2);
|
|
1536
|
+
new DataView(buffer).setUint16(0, value, false);
|
|
1537
|
+
return new Uint8Array(buffer);
|
|
1538
|
+
}
|
|
1539
|
+
function readUint16BE(data, offset) {
|
|
1540
|
+
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
1541
|
+
}
|
|
1542
|
+
function writeOQEHeader(header) {
|
|
1543
|
+
const buffer = new Uint8Array(OQE_HEADER_SIZE);
|
|
1544
|
+
let offset = 0;
|
|
1545
|
+
buffer.set(OQE_MAGIC, offset);
|
|
1546
|
+
offset += 4;
|
|
1547
|
+
buffer[offset++] = header.version;
|
|
1548
|
+
buffer[offset++] = header.algorithmSuite;
|
|
1549
|
+
buffer.set(writeUint32BE(header.flags), offset);
|
|
1550
|
+
offset += 4;
|
|
1551
|
+
buffer.set(writeUint32BE(header.metadataLength), offset);
|
|
1552
|
+
offset += 4;
|
|
1553
|
+
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
1554
|
+
offset += 4;
|
|
1555
|
+
buffer.set(header.iv, offset);
|
|
1556
|
+
return buffer;
|
|
1557
|
+
}
|
|
1558
|
+
function parseOQEHeader(data) {
|
|
1559
|
+
if (data.length < OQE_HEADER_SIZE) {
|
|
1560
|
+
throw new OQEError("INVALID_HEADER", `File too small: need ${OQE_HEADER_SIZE} bytes, got ${data.length}`);
|
|
1561
|
+
}
|
|
1562
|
+
let offset = 0;
|
|
1563
|
+
const magic = data.slice(0, 4);
|
|
1564
|
+
if (!magic.every((b, i) => b === OQE_MAGIC[i])) {
|
|
1565
|
+
throw new OQEError("INVALID_MAGIC", "Not a valid OQE file (invalid magic bytes)");
|
|
1566
|
+
}
|
|
1567
|
+
offset += 4;
|
|
1568
|
+
const version = data[offset++];
|
|
1569
|
+
if (version !== OQE_FORMAT_VERSION) {
|
|
1570
|
+
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
1571
|
+
}
|
|
1572
|
+
const algorithmSuiteRaw = data[offset++];
|
|
1573
|
+
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1574
|
+
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
1575
|
+
}
|
|
1576
|
+
const algorithmSuite = algorithmSuiteRaw;
|
|
1577
|
+
const flags = readUint32BE(data, offset);
|
|
1578
|
+
offset += 4;
|
|
1579
|
+
const metadataLength = readUint32BE(data, offset);
|
|
1580
|
+
offset += 4;
|
|
1581
|
+
const keyMaterialLength = readUint32BE(data, offset);
|
|
1582
|
+
offset += 4;
|
|
1583
|
+
const iv = data.slice(offset, offset + 12);
|
|
1584
|
+
return {
|
|
1585
|
+
version,
|
|
1586
|
+
algorithmSuite,
|
|
1587
|
+
flags,
|
|
1588
|
+
metadataLength,
|
|
1589
|
+
keyMaterialLength,
|
|
1590
|
+
iv
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function serializeHybridKeyMaterial(km) {
|
|
1594
|
+
const parts = [];
|
|
1595
|
+
parts.push(km.x25519EphemeralPk);
|
|
1596
|
+
parts.push(km.x25519Nonce);
|
|
1597
|
+
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
1598
|
+
parts.push(km.x25519WrappedKey);
|
|
1599
|
+
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
1600
|
+
parts.push(km.kyberCiphertext);
|
|
1601
|
+
parts.push(km.kyberNonce);
|
|
1602
|
+
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
1603
|
+
parts.push(km.kyberWrappedKey);
|
|
1604
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
1605
|
+
const result = new Uint8Array(totalLength);
|
|
1606
|
+
let offset = 0;
|
|
1607
|
+
for (const part of parts) {
|
|
1608
|
+
result.set(part, offset);
|
|
1609
|
+
offset += part.length;
|
|
1610
|
+
}
|
|
1611
|
+
return result;
|
|
1612
|
+
}
|
|
1613
|
+
function parseHybridKeyMaterial(data) {
|
|
1614
|
+
let offset = 0;
|
|
1615
|
+
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
1616
|
+
offset += 32;
|
|
1617
|
+
const x25519Nonce = data.slice(offset, offset + 24);
|
|
1618
|
+
offset += 24;
|
|
1619
|
+
const x25519WrappedLen = readUint16BE(data, offset);
|
|
1620
|
+
offset += 2;
|
|
1621
|
+
const x25519WrappedKey = data.slice(offset, offset + x25519WrappedLen);
|
|
1622
|
+
offset += x25519WrappedLen;
|
|
1623
|
+
const kyberCtLen = readUint16BE(data, offset);
|
|
1624
|
+
offset += 2;
|
|
1625
|
+
const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
|
|
1626
|
+
offset += kyberCtLen;
|
|
1627
|
+
const kyberNonce = data.slice(offset, offset + 24);
|
|
1628
|
+
offset += 24;
|
|
1629
|
+
const kyberWrappedLen = readUint16BE(data, offset);
|
|
1630
|
+
offset += 2;
|
|
1631
|
+
const kyberWrappedKey = data.slice(offset, offset + kyberWrappedLen);
|
|
1632
|
+
return {
|
|
1633
|
+
x25519EphemeralPk,
|
|
1634
|
+
x25519Nonce,
|
|
1635
|
+
x25519WrappedKey,
|
|
1636
|
+
kyberCiphertext,
|
|
1637
|
+
kyberNonce,
|
|
1638
|
+
kyberWrappedKey
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function serializePasswordKeyMaterial(km) {
|
|
1642
|
+
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
1643
|
+
result.set(km.salt, 0);
|
|
1644
|
+
result.set(writeUint32BE(km.memoryCost), 32);
|
|
1645
|
+
result.set(writeUint32BE(km.timeCost), 36);
|
|
1646
|
+
result.set(writeUint32BE(km.parallelism), 40);
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
function parsePasswordKeyMaterial(data) {
|
|
1650
|
+
return {
|
|
1651
|
+
salt: data.slice(0, 32),
|
|
1652
|
+
memoryCost: readUint32BE(data, 32),
|
|
1653
|
+
timeCost: readUint32BE(data, 36),
|
|
1654
|
+
parallelism: readUint32BE(data, 40)
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
function serializeMetadata(metadata) {
|
|
1658
|
+
const json = JSON.stringify(metadata);
|
|
1659
|
+
return textEncoder.encode(json);
|
|
1660
|
+
}
|
|
1661
|
+
function parseMetadata(data) {
|
|
1662
|
+
const json = textDecoder.decode(data);
|
|
1663
|
+
return JSON.parse(json);
|
|
1664
|
+
}
|
|
1665
|
+
function assembleOQEFile(components) {
|
|
1666
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = components;
|
|
1667
|
+
const headerBytes = writeOQEHeader(header);
|
|
1668
|
+
const totalLength = headerBytes.length + keyMaterial.length + encryptedMetadata.length + encryptedContent.length;
|
|
1669
|
+
const result = new Uint8Array(totalLength);
|
|
1670
|
+
let offset = 0;
|
|
1671
|
+
result.set(headerBytes, offset);
|
|
1672
|
+
offset += headerBytes.length;
|
|
1673
|
+
result.set(keyMaterial, offset);
|
|
1674
|
+
offset += keyMaterial.length;
|
|
1675
|
+
result.set(encryptedMetadata, offset);
|
|
1676
|
+
offset += encryptedMetadata.length;
|
|
1677
|
+
result.set(encryptedContent, offset);
|
|
1678
|
+
return result;
|
|
1679
|
+
}
|
|
1680
|
+
function parseOQEFile(data) {
|
|
1681
|
+
const header = parseOQEHeader(data);
|
|
1682
|
+
let offset = OQE_HEADER_SIZE;
|
|
1683
|
+
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
1684
|
+
offset += header.keyMaterialLength;
|
|
1685
|
+
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
1686
|
+
offset += header.metadataLength;
|
|
1687
|
+
const encryptedContent = data.slice(offset);
|
|
1688
|
+
return {
|
|
1689
|
+
header,
|
|
1690
|
+
keyMaterial,
|
|
1691
|
+
encryptedMetadata,
|
|
1692
|
+
encryptedContent
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
var OQE_EXTENSION = ".oqe";
|
|
1696
|
+
function addOQEExtension(filename) {
|
|
1697
|
+
if (filename.toLowerCase().endsWith(OQE_EXTENSION)) {
|
|
1698
|
+
return filename;
|
|
1699
|
+
}
|
|
1700
|
+
return `${filename}${OQE_EXTENSION}`;
|
|
1701
|
+
}
|
|
1702
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
1703
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1704
|
+
}
|
|
1705
|
+
function computeIdentityHash(publicKeyHex) {
|
|
1706
|
+
const hash = sha256(fromHex(publicKeyHex));
|
|
1707
|
+
return toHex(hash).slice(0, 16);
|
|
1708
|
+
}
|
|
1709
|
+
async function encryptHybrid(plaintext, metadata, options) {
|
|
1710
|
+
if (!await isKyberAvailable()) {
|
|
1711
|
+
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
1712
|
+
}
|
|
1713
|
+
const contentKey = rand32();
|
|
1714
|
+
const iv = rand12();
|
|
1715
|
+
const x25519EphKp = nacl4.box.keyPair();
|
|
1716
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
1717
|
+
const x25519Shared = nacl4.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
1718
|
+
const x25519Kek = hkdfFlex2(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
1719
|
+
const x25519Nonce = rand24();
|
|
1720
|
+
const x25519WrappedKey = nacl4.secretbox(contentKey, x25519Nonce, x25519Kek);
|
|
1721
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
1722
|
+
const kyberKek = hkdfFlex2(kyberResult.sharedSecret, "omnituum/fs/kyber", "wrap-content-key");
|
|
1723
|
+
const kyberNonce = rand24();
|
|
1724
|
+
const kyberWrappedKey = nacl4.secretbox(contentKey, kyberNonce, kyberKek);
|
|
1725
|
+
const keyMaterial = {
|
|
1726
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
1727
|
+
x25519Nonce,
|
|
1728
|
+
x25519WrappedKey,
|
|
1729
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
1730
|
+
kyberNonce,
|
|
1731
|
+
kyberWrappedKey
|
|
1732
|
+
};
|
|
1733
|
+
const keyMaterialBytes = serializeHybridKeyMaterial(keyMaterial);
|
|
1734
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1735
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
1736
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
1737
|
+
const header = {
|
|
1738
|
+
version: OQE_FORMAT_VERSION,
|
|
1739
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM,
|
|
1740
|
+
flags: 0,
|
|
1741
|
+
metadataLength: encryptedMetadata.length,
|
|
1742
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1743
|
+
iv
|
|
1744
|
+
};
|
|
1745
|
+
const fileData = assembleOQEFile({
|
|
1746
|
+
header,
|
|
1747
|
+
keyMaterial: keyMaterialBytes,
|
|
1748
|
+
encryptedMetadata,
|
|
1749
|
+
encryptedContent
|
|
1750
|
+
});
|
|
1751
|
+
return {
|
|
1752
|
+
data: fileData,
|
|
1753
|
+
filename: addOQEExtension(metadata.filename),
|
|
1754
|
+
metadata,
|
|
1755
|
+
mode: "hybrid"
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
async function encryptPassword(plaintext, metadata, options) {
|
|
1759
|
+
if (!await isArgon2Available()) {
|
|
1760
|
+
throw new OQEError("ARGON2_UNAVAILABLE", "Argon2 library not available in this environment");
|
|
1761
|
+
}
|
|
1762
|
+
const params = {
|
|
1763
|
+
...DEFAULT_ARGON2ID_PARAMS,
|
|
1764
|
+
...options.argon2Params
|
|
1765
|
+
};
|
|
1766
|
+
const salt = generateArgon2Salt(params.saltLength);
|
|
1767
|
+
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
1768
|
+
const iv = rand12();
|
|
1769
|
+
const keyMaterial = {
|
|
1770
|
+
salt,
|
|
1771
|
+
memoryCost: params.memoryCost,
|
|
1772
|
+
timeCost: params.timeCost,
|
|
1773
|
+
parallelism: params.parallelism
|
|
1774
|
+
};
|
|
1775
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
1776
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
1777
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
1778
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
1779
|
+
const header = {
|
|
1780
|
+
version: OQE_FORMAT_VERSION,
|
|
1781
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
1782
|
+
flags: 0,
|
|
1783
|
+
metadataLength: encryptedMetadata.length,
|
|
1784
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
1785
|
+
iv
|
|
1786
|
+
};
|
|
1787
|
+
const fileData = assembleOQEFile({
|
|
1788
|
+
header,
|
|
1789
|
+
keyMaterial: keyMaterialBytes,
|
|
1790
|
+
encryptedMetadata,
|
|
1791
|
+
encryptedContent
|
|
1792
|
+
});
|
|
1793
|
+
return {
|
|
1794
|
+
data: fileData,
|
|
1795
|
+
filename: addOQEExtension(metadata.filename),
|
|
1796
|
+
metadata,
|
|
1797
|
+
mode: "password"
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
async function encryptFile(input, options) {
|
|
1801
|
+
const plaintext = await toUint8Array(input.data);
|
|
1802
|
+
const metadata = {
|
|
1803
|
+
filename: input.filename,
|
|
1804
|
+
originalSize: plaintext.length,
|
|
1805
|
+
mimeType: input.mimeType,
|
|
1806
|
+
encryptedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1807
|
+
};
|
|
1808
|
+
if (options.mode === "hybrid") {
|
|
1809
|
+
metadata.recipientIdHash = computeIdentityHash(options.recipientPublicKeys.x25519PubHex);
|
|
1810
|
+
if (options.sender) {
|
|
1811
|
+
metadata.encryptorIdHash = options.sender.id;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
if (options.mode === "hybrid") {
|
|
1815
|
+
return encryptHybrid(plaintext, metadata, options);
|
|
1816
|
+
} else {
|
|
1817
|
+
return encryptPassword(plaintext, metadata, options);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
async function encryptFileWithPassword(input, password) {
|
|
1821
|
+
return encryptFile(input, {
|
|
1822
|
+
mode: "password",
|
|
1823
|
+
password
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
function hkdfFlex3(ikm, salt, info) {
|
|
1827
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
1828
|
+
}
|
|
1829
|
+
async function decryptHybrid(encryptedData, options) {
|
|
1830
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
1831
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
1832
|
+
throw new OQEError(
|
|
1833
|
+
"UNSUPPORTED_ALGORITHM",
|
|
1834
|
+
"This file was not encrypted with hybrid mode. Use password decryption."
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
const km = parseHybridKeyMaterial(keyMaterial);
|
|
1838
|
+
let contentKey = null;
|
|
1839
|
+
if (await isKyberAvailable()) {
|
|
1840
|
+
try {
|
|
1841
|
+
const kyberShared = await kyberDecapsulate(
|
|
1842
|
+
toB64(km.kyberCiphertext),
|
|
1843
|
+
options.recipientSecretKeys.kyberSecB64
|
|
1844
|
+
);
|
|
1845
|
+
const kyberKek = hkdfFlex3(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
|
|
1846
|
+
const unwrapped = nacl4.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
1847
|
+
if (unwrapped) {
|
|
1848
|
+
contentKey = unwrapped;
|
|
1849
|
+
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
1850
|
+
}
|
|
1851
|
+
} catch (e) {
|
|
1852
|
+
console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (!contentKey) {
|
|
1856
|
+
try {
|
|
1857
|
+
const ephPk = km.x25519EphemeralPk;
|
|
1858
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
1859
|
+
const x25519Shared = nacl4.scalarMult(sk, ephPk);
|
|
1860
|
+
const x25519Kek = hkdfFlex3(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
1861
|
+
const unwrapped = nacl4.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
1862
|
+
if (unwrapped) {
|
|
1863
|
+
contentKey = unwrapped;
|
|
1864
|
+
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
1865
|
+
}
|
|
1866
|
+
} catch (e) {
|
|
1867
|
+
console.warn("[OQE] X25519 decryption failed:", e);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (!contentKey) {
|
|
1871
|
+
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
1872
|
+
}
|
|
1873
|
+
let metadata;
|
|
1874
|
+
try {
|
|
1875
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
1876
|
+
metadata = parseMetadata(metadataBytes);
|
|
1877
|
+
} catch (e) {
|
|
1878
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file metadata");
|
|
1879
|
+
}
|
|
1880
|
+
let plaintext;
|
|
1881
|
+
try {
|
|
1882
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
1885
|
+
}
|
|
1886
|
+
return {
|
|
1887
|
+
data: plaintext,
|
|
1888
|
+
filename: metadata.filename,
|
|
1889
|
+
mimeType: metadata.mimeType,
|
|
1890
|
+
originalSize: metadata.originalSize,
|
|
1891
|
+
metadata,
|
|
1892
|
+
mode: "hybrid"
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
async function decryptPassword(encryptedData, options) {
|
|
1896
|
+
if (!await isArgon2Available()) {
|
|
1897
|
+
throw new OQEError("ARGON2_UNAVAILABLE", "Argon2 library not available in this environment");
|
|
1898
|
+
}
|
|
1899
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
1900
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
1901
|
+
throw new OQEError(
|
|
1902
|
+
"UNSUPPORTED_ALGORITHM",
|
|
1903
|
+
"This file was not encrypted with password mode. Use hybrid decryption."
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
const km = parsePasswordKeyMaterial(keyMaterial);
|
|
1907
|
+
const contentKey = await deriveKeyFromPassword(options.password, km.salt, {
|
|
1908
|
+
memoryCost: km.memoryCost,
|
|
1909
|
+
timeCost: km.timeCost,
|
|
1910
|
+
parallelism: km.parallelism,
|
|
1911
|
+
hashLength: 32,
|
|
1912
|
+
saltLength: km.salt.length
|
|
1913
|
+
});
|
|
1914
|
+
let metadata;
|
|
1915
|
+
try {
|
|
1916
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
1917
|
+
metadata = parseMetadata(metadataBytes);
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
1920
|
+
}
|
|
1921
|
+
let plaintext;
|
|
1922
|
+
try {
|
|
1923
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
data: plaintext,
|
|
1929
|
+
filename: metadata.filename,
|
|
1930
|
+
mimeType: metadata.mimeType,
|
|
1931
|
+
originalSize: metadata.originalSize,
|
|
1932
|
+
metadata,
|
|
1933
|
+
mode: "password"
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
async function decryptFile(encryptedData, options) {
|
|
1937
|
+
const data = await toUint8Array(encryptedData);
|
|
1938
|
+
if (options.mode === "hybrid") {
|
|
1939
|
+
return decryptHybrid(data, options);
|
|
1940
|
+
} else {
|
|
1941
|
+
return decryptPassword(data, options);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function decryptFileWithPassword(encryptedData, password) {
|
|
1945
|
+
return decryptFile(encryptedData, {
|
|
1946
|
+
mode: "password",
|
|
1947
|
+
password
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/tunnel/types.ts
|
|
1952
|
+
var TUNNEL_VERSION = "omnituum.tunnel.v1";
|
|
1953
|
+
var TUNNEL_KEY_SIZE = 32;
|
|
1954
|
+
var TUNNEL_NONCE_SIZE = 24;
|
|
1955
|
+
|
|
1956
|
+
// src/tunnel/session.ts
|
|
1957
|
+
function deriveNonce(base, counter) {
|
|
1958
|
+
const nonce = new Uint8Array(24);
|
|
1959
|
+
nonce.set(base.subarray(0, 16), 0);
|
|
1960
|
+
const view = new DataView(nonce.buffer, 16, 8);
|
|
1961
|
+
const baseView = new DataView(base.buffer, base.byteOffset + 16, 8);
|
|
1962
|
+
const baseValue = baseView.getBigUint64(0, false);
|
|
1963
|
+
view.setBigUint64(0, baseValue ^ counter, false);
|
|
1964
|
+
return nonce;
|
|
1965
|
+
}
|
|
1966
|
+
function validateKeyMaterial(keys) {
|
|
1967
|
+
if (!keys.sendKey || keys.sendKey.length !== TUNNEL_KEY_SIZE) {
|
|
1968
|
+
throw new Error(`sendKey must be ${TUNNEL_KEY_SIZE} bytes`);
|
|
1969
|
+
}
|
|
1970
|
+
if (!keys.recvKey || keys.recvKey.length !== TUNNEL_KEY_SIZE) {
|
|
1971
|
+
throw new Error(`recvKey must be ${TUNNEL_KEY_SIZE} bytes`);
|
|
1972
|
+
}
|
|
1973
|
+
if (!keys.sendBaseNonce || keys.sendBaseNonce.length !== TUNNEL_NONCE_SIZE) {
|
|
1974
|
+
throw new Error(`sendBaseNonce must be ${TUNNEL_NONCE_SIZE} bytes`);
|
|
1975
|
+
}
|
|
1976
|
+
if (!keys.recvBaseNonce || keys.recvBaseNonce.length !== TUNNEL_NONCE_SIZE) {
|
|
1977
|
+
throw new Error(`recvBaseNonce must be ${TUNNEL_NONCE_SIZE} bytes`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function createTunnelSession(keys) {
|
|
1981
|
+
validateKeyMaterial(keys);
|
|
1982
|
+
const sendKey = new Uint8Array(keys.sendKey);
|
|
1983
|
+
const recvKey = new Uint8Array(keys.recvKey);
|
|
1984
|
+
const sendBaseNonce = new Uint8Array(keys.sendBaseNonce);
|
|
1985
|
+
const recvBaseNonce = new Uint8Array(keys.recvBaseNonce);
|
|
1986
|
+
let sendCounter = 0n;
|
|
1987
|
+
let recvCounter = 0n;
|
|
1988
|
+
let closed = false;
|
|
1989
|
+
function assertOpen() {
|
|
1990
|
+
if (closed) {
|
|
1991
|
+
throw new Error("Tunnel is closed");
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return {
|
|
1995
|
+
encrypt(plaintext, aad) {
|
|
1996
|
+
assertOpen();
|
|
1997
|
+
const nonce = deriveNonce(sendBaseNonce, sendCounter);
|
|
1998
|
+
const ciphertext = xChaCha20Poly1305Encrypt(sendKey, nonce, plaintext, aad);
|
|
1999
|
+
sendCounter++;
|
|
2000
|
+
zeroMemory(nonce);
|
|
2001
|
+
return ciphertext;
|
|
2002
|
+
},
|
|
2003
|
+
decrypt(ciphertext, aad) {
|
|
2004
|
+
assertOpen();
|
|
2005
|
+
const nonce = deriveNonce(recvBaseNonce, recvCounter);
|
|
2006
|
+
const plaintext = xChaCha20Poly1305Decrypt(recvKey, nonce, ciphertext, aad);
|
|
2007
|
+
recvCounter++;
|
|
2008
|
+
zeroMemory(nonce);
|
|
2009
|
+
return plaintext;
|
|
2010
|
+
},
|
|
2011
|
+
close() {
|
|
2012
|
+
if (closed) return;
|
|
2013
|
+
closed = true;
|
|
2014
|
+
zeroMemory(sendKey);
|
|
2015
|
+
zeroMemory(recvKey);
|
|
2016
|
+
zeroMemory(sendBaseNonce);
|
|
2017
|
+
zeroMemory(recvBaseNonce);
|
|
2018
|
+
},
|
|
2019
|
+
get isOpen() {
|
|
2020
|
+
return !closed;
|
|
2021
|
+
},
|
|
2022
|
+
get sendCounter() {
|
|
2023
|
+
return sendCounter;
|
|
2024
|
+
},
|
|
2025
|
+
get recvCounter() {
|
|
2026
|
+
return recvCounter;
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
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, 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, 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 };
|