@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/fs/index.js
ADDED
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
import { argon2id } from 'hash-wasm';
|
|
2
|
+
import { sha256 as sha256$1 } from '@noble/hashes/sha256';
|
|
3
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
4
|
+
import nacl2 from 'tweetnacl';
|
|
5
|
+
|
|
6
|
+
// src/fs/types.ts
|
|
7
|
+
var OQE_MAGIC = new Uint8Array([79, 81, 69, 70]);
|
|
8
|
+
var OQE_FORMAT_VERSION = 1;
|
|
9
|
+
var ALGORITHM_SUITES = {
|
|
10
|
+
/** Hybrid: X25519 ECDH + Kyber768 KEM + AES-256-GCM */
|
|
11
|
+
HYBRID_X25519_KYBER768_AES256GCM: 1,
|
|
12
|
+
/** Password: Argon2id + AES-256-GCM */
|
|
13
|
+
PASSWORD_ARGON2ID_AES256GCM: 2
|
|
14
|
+
};
|
|
15
|
+
var DEFAULT_ARGON2ID_PARAMS = {
|
|
16
|
+
memoryCost: 65536,
|
|
17
|
+
// 64 MB
|
|
18
|
+
timeCost: 3,
|
|
19
|
+
parallelism: 4,
|
|
20
|
+
hashLength: 32,
|
|
21
|
+
saltLength: 32
|
|
22
|
+
};
|
|
23
|
+
var MIN_ARGON2ID_PARAMS = {
|
|
24
|
+
memoryCost: 19456,
|
|
25
|
+
// ~19 MB (OWASP minimum)
|
|
26
|
+
timeCost: 2,
|
|
27
|
+
parallelism: 1,
|
|
28
|
+
hashLength: 32,
|
|
29
|
+
saltLength: 32
|
|
30
|
+
};
|
|
31
|
+
var OQE_HEADER_SIZE = 30;
|
|
32
|
+
var OQEError = class extends Error {
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.name = "OQEError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
async function toUint8Array(input) {
|
|
40
|
+
if (input instanceof Uint8Array) {
|
|
41
|
+
return input;
|
|
42
|
+
}
|
|
43
|
+
if (input instanceof ArrayBuffer) {
|
|
44
|
+
return new Uint8Array(input);
|
|
45
|
+
}
|
|
46
|
+
const buffer = await input.arrayBuffer();
|
|
47
|
+
return new Uint8Array(buffer);
|
|
48
|
+
}
|
|
49
|
+
var textEncoder = new TextEncoder();
|
|
50
|
+
var textDecoder = new TextDecoder();
|
|
51
|
+
function toB64(bytes) {
|
|
52
|
+
let binary = "";
|
|
53
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
54
|
+
binary += String.fromCharCode(bytes[i]);
|
|
55
|
+
}
|
|
56
|
+
return btoa(binary);
|
|
57
|
+
}
|
|
58
|
+
function fromB64(str) {
|
|
59
|
+
const binary = atob(str);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let i = 0; i < binary.length; i++) {
|
|
62
|
+
bytes[i] = binary.charCodeAt(i);
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
function toHex(bytes) {
|
|
67
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
68
|
+
}
|
|
69
|
+
function fromHex(hex) {
|
|
70
|
+
const s = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
71
|
+
const normalized = s.length % 2 ? "0" + s : s;
|
|
72
|
+
const out = new Uint8Array(normalized.length / 2);
|
|
73
|
+
for (let i = 0; i < out.length; i++) {
|
|
74
|
+
out[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function rand32() {
|
|
79
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
80
|
+
}
|
|
81
|
+
function rand24() {
|
|
82
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(24));
|
|
83
|
+
}
|
|
84
|
+
function rand12() {
|
|
85
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
86
|
+
}
|
|
87
|
+
function randN(n) {
|
|
88
|
+
return globalThis.crypto.getRandomValues(new Uint8Array(n));
|
|
89
|
+
}
|
|
90
|
+
function sha256(bytes) {
|
|
91
|
+
return sha256$1(bytes);
|
|
92
|
+
}
|
|
93
|
+
function hkdfSha256(ikm, opts) {
|
|
94
|
+
const salt = opts?.salt ?? new Uint8Array(32);
|
|
95
|
+
const info = opts?.info ?? new Uint8Array(0);
|
|
96
|
+
const L = opts?.length ?? 32;
|
|
97
|
+
const prk = hmac(sha256$1, salt, ikm);
|
|
98
|
+
let t = new Uint8Array(0);
|
|
99
|
+
const chunks = [];
|
|
100
|
+
for (let i = 1; i <= Math.ceil(L / 32); i++) {
|
|
101
|
+
const input = new Uint8Array(t.length + info.length + 1);
|
|
102
|
+
input.set(t, 0);
|
|
103
|
+
input.set(info, t.length);
|
|
104
|
+
input[input.length - 1] = i;
|
|
105
|
+
t = new Uint8Array(hmac(sha256$1, prk, input));
|
|
106
|
+
chunks.push(t);
|
|
107
|
+
}
|
|
108
|
+
const out = new Uint8Array(L);
|
|
109
|
+
let off = 0;
|
|
110
|
+
for (const c of chunks) {
|
|
111
|
+
out.set(c.subarray(0, L - off), off);
|
|
112
|
+
off += c.length;
|
|
113
|
+
if (off >= L) break;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
var ub64 = fromB64;
|
|
118
|
+
var u8 = (s) => typeof s === "string" ? textEncoder.encode(s) : s;
|
|
119
|
+
|
|
120
|
+
// src/fs/argon2.ts
|
|
121
|
+
async function deriveKeyFromPassword(password, salt, params = DEFAULT_ARGON2ID_PARAMS) {
|
|
122
|
+
if (salt.length !== params.saltLength) {
|
|
123
|
+
throw new Error(`Salt must be ${params.saltLength} bytes, got ${salt.length}`);
|
|
124
|
+
}
|
|
125
|
+
const hash = await argon2id({
|
|
126
|
+
password,
|
|
127
|
+
salt,
|
|
128
|
+
parallelism: params.parallelism,
|
|
129
|
+
iterations: params.timeCost,
|
|
130
|
+
memorySize: params.memoryCost,
|
|
131
|
+
hashLength: params.hashLength,
|
|
132
|
+
outputType: "binary"
|
|
133
|
+
});
|
|
134
|
+
return new Uint8Array(hash);
|
|
135
|
+
}
|
|
136
|
+
function generateArgon2Salt(length = 32) {
|
|
137
|
+
return randN(length);
|
|
138
|
+
}
|
|
139
|
+
async function verifyPassword(password, salt, expectedKey, params = DEFAULT_ARGON2ID_PARAMS) {
|
|
140
|
+
const derivedKey = await deriveKeyFromPassword(password, salt, params);
|
|
141
|
+
if (derivedKey.length !== expectedKey.length) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
let result = 0;
|
|
145
|
+
for (let i = 0; i < derivedKey.length; i++) {
|
|
146
|
+
result |= derivedKey[i] ^ expectedKey[i];
|
|
147
|
+
}
|
|
148
|
+
return result === 0;
|
|
149
|
+
}
|
|
150
|
+
function estimateArgon2Params(targetTimeMs = 1e3, availableMemoryMB = 64) {
|
|
151
|
+
const params = { ...MIN_ARGON2ID_PARAMS };
|
|
152
|
+
const maxMemoryKB = Math.min(availableMemoryMB * 1024, 65536);
|
|
153
|
+
params.memoryCost = Math.max(MIN_ARGON2ID_PARAMS.memoryCost, maxMemoryKB);
|
|
154
|
+
params.parallelism = Math.min(4, navigator.hardwareConcurrency || 1);
|
|
155
|
+
const estimatedIterations = Math.max(2, Math.floor(targetTimeMs / 300));
|
|
156
|
+
params.timeCost = Math.min(estimatedIterations, 10);
|
|
157
|
+
return params;
|
|
158
|
+
}
|
|
159
|
+
async function benchmarkArgon2(params = DEFAULT_ARGON2ID_PARAMS) {
|
|
160
|
+
const testPassword = "benchmark-test-password";
|
|
161
|
+
const testSalt = generateArgon2Salt(params.saltLength);
|
|
162
|
+
const start = performance.now();
|
|
163
|
+
await deriveKeyFromPassword(testPassword, testSalt, params);
|
|
164
|
+
const end = performance.now();
|
|
165
|
+
return end - start;
|
|
166
|
+
}
|
|
167
|
+
var _argon2Available = null;
|
|
168
|
+
async function isArgon2Available() {
|
|
169
|
+
if (_argon2Available !== null) {
|
|
170
|
+
return _argon2Available;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
await argon2id({
|
|
174
|
+
password: "test",
|
|
175
|
+
salt: new Uint8Array(16),
|
|
176
|
+
parallelism: 1,
|
|
177
|
+
iterations: 1,
|
|
178
|
+
memorySize: 1024,
|
|
179
|
+
// 1 MB
|
|
180
|
+
hashLength: 32,
|
|
181
|
+
outputType: "binary"
|
|
182
|
+
});
|
|
183
|
+
_argon2Available = true;
|
|
184
|
+
} catch {
|
|
185
|
+
_argon2Available = false;
|
|
186
|
+
}
|
|
187
|
+
return _argon2Available;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/fs/aes.ts
|
|
191
|
+
function toArrayBuffer(arr) {
|
|
192
|
+
if (arr.buffer instanceof SharedArrayBuffer || arr.byteOffset !== 0 || arr.byteLength !== arr.buffer.byteLength) {
|
|
193
|
+
const copy = new ArrayBuffer(arr.byteLength);
|
|
194
|
+
new Uint8Array(copy).set(arr);
|
|
195
|
+
return copy;
|
|
196
|
+
}
|
|
197
|
+
return arr.buffer;
|
|
198
|
+
}
|
|
199
|
+
var AES_KEY_SIZE = 32;
|
|
200
|
+
var AES_GCM_IV_SIZE = 12;
|
|
201
|
+
var AES_GCM_TAG_SIZE = 16;
|
|
202
|
+
async function importAesKey(keyBytes) {
|
|
203
|
+
if (keyBytes.length !== AES_KEY_SIZE) {
|
|
204
|
+
throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
|
|
205
|
+
}
|
|
206
|
+
return globalThis.crypto.subtle.importKey(
|
|
207
|
+
"raw",
|
|
208
|
+
toArrayBuffer(keyBytes),
|
|
209
|
+
{ name: "AES-GCM", length: 256 },
|
|
210
|
+
false,
|
|
211
|
+
// not extractable
|
|
212
|
+
["encrypt", "decrypt"]
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
async function generateAesKey() {
|
|
216
|
+
return globalThis.crypto.subtle.generateKey(
|
|
217
|
+
{ name: "AES-GCM", length: 256 },
|
|
218
|
+
true,
|
|
219
|
+
// extractable for wrapping
|
|
220
|
+
["encrypt", "decrypt"]
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
async function exportAesKey(key) {
|
|
224
|
+
const exported = await globalThis.crypto.subtle.exportKey("raw", key);
|
|
225
|
+
return new Uint8Array(exported);
|
|
226
|
+
}
|
|
227
|
+
async function aesEncrypt(plaintext, key, iv, additionalData) {
|
|
228
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
229
|
+
const ivBytes = iv ?? rand12();
|
|
230
|
+
if (ivBytes.length !== AES_GCM_IV_SIZE) {
|
|
231
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${ivBytes.length}`);
|
|
232
|
+
}
|
|
233
|
+
const encrypted = await globalThis.crypto.subtle.encrypt(
|
|
234
|
+
{
|
|
235
|
+
name: "AES-GCM",
|
|
236
|
+
iv: toArrayBuffer(ivBytes),
|
|
237
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
238
|
+
tagLength: 128
|
|
239
|
+
// 16 bytes
|
|
240
|
+
},
|
|
241
|
+
cryptoKey,
|
|
242
|
+
toArrayBuffer(plaintext)
|
|
243
|
+
);
|
|
244
|
+
return {
|
|
245
|
+
iv: ivBytes,
|
|
246
|
+
ciphertext: new Uint8Array(encrypted)
|
|
247
|
+
// Includes auth tag
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function aesEncryptCombined(plaintext, key, additionalData) {
|
|
251
|
+
const { iv, ciphertext } = await aesEncrypt(plaintext, key, void 0, additionalData);
|
|
252
|
+
const combined = new Uint8Array(iv.length + ciphertext.length);
|
|
253
|
+
combined.set(iv, 0);
|
|
254
|
+
combined.set(ciphertext, iv.length);
|
|
255
|
+
return combined;
|
|
256
|
+
}
|
|
257
|
+
async function aesDecrypt(ciphertext, key, iv, additionalData) {
|
|
258
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
259
|
+
if (iv.length !== AES_GCM_IV_SIZE) {
|
|
260
|
+
throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${iv.length}`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const decrypted = await globalThis.crypto.subtle.decrypt(
|
|
264
|
+
{
|
|
265
|
+
name: "AES-GCM",
|
|
266
|
+
iv: toArrayBuffer(iv),
|
|
267
|
+
additionalData: additionalData ? toArrayBuffer(additionalData) : void 0,
|
|
268
|
+
tagLength: 128
|
|
269
|
+
},
|
|
270
|
+
cryptoKey,
|
|
271
|
+
toArrayBuffer(ciphertext)
|
|
272
|
+
);
|
|
273
|
+
return new Uint8Array(decrypted);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
throw new Error("Decryption failed: authentication tag mismatch (wrong key or corrupted data)");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function aesDecryptCombined(combined, key, additionalData) {
|
|
279
|
+
if (combined.length < AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE) {
|
|
280
|
+
throw new Error(`Combined data too short: need at least ${AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE} bytes`);
|
|
281
|
+
}
|
|
282
|
+
const iv = combined.slice(0, AES_GCM_IV_SIZE);
|
|
283
|
+
const ciphertext = combined.slice(AES_GCM_IV_SIZE);
|
|
284
|
+
return aesDecrypt(ciphertext, key, iv, additionalData);
|
|
285
|
+
}
|
|
286
|
+
var STREAM_CHUNK_SIZE = 1024 * 1024;
|
|
287
|
+
async function aesEncryptStreaming(plaintext, key, onProgress) {
|
|
288
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
289
|
+
const chunks = [];
|
|
290
|
+
const totalChunks = Math.ceil(plaintext.length / STREAM_CHUNK_SIZE);
|
|
291
|
+
const header = new Uint8Array(4);
|
|
292
|
+
new DataView(header.buffer).setUint32(0, totalChunks, false);
|
|
293
|
+
chunks.push(header);
|
|
294
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
295
|
+
const start = i * STREAM_CHUNK_SIZE;
|
|
296
|
+
const end = Math.min(start + STREAM_CHUNK_SIZE, plaintext.length);
|
|
297
|
+
const chunk = plaintext.slice(start, end);
|
|
298
|
+
const { iv, ciphertext } = await aesEncrypt(chunk, cryptoKey);
|
|
299
|
+
const chunkData = new Uint8Array(12 + 4 + ciphertext.length);
|
|
300
|
+
chunkData.set(iv, 0);
|
|
301
|
+
new DataView(chunkData.buffer).setUint32(12, ciphertext.length, false);
|
|
302
|
+
chunkData.set(ciphertext, 16);
|
|
303
|
+
chunks.push(chunkData);
|
|
304
|
+
if (onProgress) {
|
|
305
|
+
onProgress(Math.round((i + 1) / totalChunks * 100));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
309
|
+
const result = new Uint8Array(totalLength);
|
|
310
|
+
let offset = 0;
|
|
311
|
+
for (const chunk of chunks) {
|
|
312
|
+
result.set(chunk, offset);
|
|
313
|
+
offset += chunk.length;
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
async function aesDecryptStreaming(encrypted, key, onProgress) {
|
|
318
|
+
const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
|
|
319
|
+
const totalChunks = new DataView(encrypted.buffer, encrypted.byteOffset).getUint32(0, false);
|
|
320
|
+
const chunks = [];
|
|
321
|
+
let offset = 4;
|
|
322
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
323
|
+
const iv = encrypted.slice(offset, offset + 12);
|
|
324
|
+
offset += 12;
|
|
325
|
+
const ctLength = new DataView(encrypted.buffer, encrypted.byteOffset + offset).getUint32(0, false);
|
|
326
|
+
offset += 4;
|
|
327
|
+
const ciphertext = encrypted.slice(offset, offset + ctLength);
|
|
328
|
+
offset += ctLength;
|
|
329
|
+
const plaintext = await aesDecrypt(ciphertext, cryptoKey, iv);
|
|
330
|
+
chunks.push(plaintext);
|
|
331
|
+
if (onProgress) {
|
|
332
|
+
onProgress(Math.round((i + 1) / totalChunks * 100));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
336
|
+
const result = new Uint8Array(totalLength);
|
|
337
|
+
let resultOffset = 0;
|
|
338
|
+
for (const chunk of chunks) {
|
|
339
|
+
result.set(chunk, resultOffset);
|
|
340
|
+
resultOffset += chunk.length;
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/fs/format.ts
|
|
346
|
+
function writeUint32BE(value) {
|
|
347
|
+
const buffer = new ArrayBuffer(4);
|
|
348
|
+
new DataView(buffer).setUint32(0, value, false);
|
|
349
|
+
return new Uint8Array(buffer);
|
|
350
|
+
}
|
|
351
|
+
function readUint32BE(data, offset) {
|
|
352
|
+
return new DataView(data.buffer, data.byteOffset + offset).getUint32(0, false);
|
|
353
|
+
}
|
|
354
|
+
function writeUint16BE(value) {
|
|
355
|
+
const buffer = new ArrayBuffer(2);
|
|
356
|
+
new DataView(buffer).setUint16(0, value, false);
|
|
357
|
+
return new Uint8Array(buffer);
|
|
358
|
+
}
|
|
359
|
+
function readUint16BE(data, offset) {
|
|
360
|
+
return new DataView(data.buffer, data.byteOffset + offset).getUint16(0, false);
|
|
361
|
+
}
|
|
362
|
+
function writeOQEHeader(header) {
|
|
363
|
+
const buffer = new Uint8Array(OQE_HEADER_SIZE);
|
|
364
|
+
let offset = 0;
|
|
365
|
+
buffer.set(OQE_MAGIC, offset);
|
|
366
|
+
offset += 4;
|
|
367
|
+
buffer[offset++] = header.version;
|
|
368
|
+
buffer[offset++] = header.algorithmSuite;
|
|
369
|
+
buffer.set(writeUint32BE(header.flags), offset);
|
|
370
|
+
offset += 4;
|
|
371
|
+
buffer.set(writeUint32BE(header.metadataLength), offset);
|
|
372
|
+
offset += 4;
|
|
373
|
+
buffer.set(writeUint32BE(header.keyMaterialLength), offset);
|
|
374
|
+
offset += 4;
|
|
375
|
+
buffer.set(header.iv, offset);
|
|
376
|
+
return buffer;
|
|
377
|
+
}
|
|
378
|
+
function parseOQEHeader(data) {
|
|
379
|
+
if (data.length < OQE_HEADER_SIZE) {
|
|
380
|
+
throw new OQEError("INVALID_HEADER", `File too small: need ${OQE_HEADER_SIZE} bytes, got ${data.length}`);
|
|
381
|
+
}
|
|
382
|
+
let offset = 0;
|
|
383
|
+
const magic = data.slice(0, 4);
|
|
384
|
+
if (!magic.every((b, i) => b === OQE_MAGIC[i])) {
|
|
385
|
+
throw new OQEError("INVALID_MAGIC", "Not a valid OQE file (invalid magic bytes)");
|
|
386
|
+
}
|
|
387
|
+
offset += 4;
|
|
388
|
+
const version = data[offset++];
|
|
389
|
+
if (version !== OQE_FORMAT_VERSION) {
|
|
390
|
+
throw new OQEError("UNSUPPORTED_VERSION", `Unsupported OQE version: ${version}`);
|
|
391
|
+
}
|
|
392
|
+
const algorithmSuiteRaw = data[offset++];
|
|
393
|
+
if (algorithmSuiteRaw !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM && algorithmSuiteRaw !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
394
|
+
throw new OQEError("UNSUPPORTED_ALGORITHM", `Unsupported algorithm suite: 0x${algorithmSuiteRaw.toString(16)}`);
|
|
395
|
+
}
|
|
396
|
+
const algorithmSuite = algorithmSuiteRaw;
|
|
397
|
+
const flags = readUint32BE(data, offset);
|
|
398
|
+
offset += 4;
|
|
399
|
+
const metadataLength = readUint32BE(data, offset);
|
|
400
|
+
offset += 4;
|
|
401
|
+
const keyMaterialLength = readUint32BE(data, offset);
|
|
402
|
+
offset += 4;
|
|
403
|
+
const iv = data.slice(offset, offset + 12);
|
|
404
|
+
return {
|
|
405
|
+
version,
|
|
406
|
+
algorithmSuite,
|
|
407
|
+
flags,
|
|
408
|
+
metadataLength,
|
|
409
|
+
keyMaterialLength,
|
|
410
|
+
iv
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function serializeHybridKeyMaterial(km) {
|
|
414
|
+
const parts = [];
|
|
415
|
+
parts.push(km.x25519EphemeralPk);
|
|
416
|
+
parts.push(km.x25519Nonce);
|
|
417
|
+
parts.push(writeUint16BE(km.x25519WrappedKey.length));
|
|
418
|
+
parts.push(km.x25519WrappedKey);
|
|
419
|
+
parts.push(writeUint16BE(km.kyberCiphertext.length));
|
|
420
|
+
parts.push(km.kyberCiphertext);
|
|
421
|
+
parts.push(km.kyberNonce);
|
|
422
|
+
parts.push(writeUint16BE(km.kyberWrappedKey.length));
|
|
423
|
+
parts.push(km.kyberWrappedKey);
|
|
424
|
+
const totalLength = parts.reduce((sum, p) => sum + p.length, 0);
|
|
425
|
+
const result = new Uint8Array(totalLength);
|
|
426
|
+
let offset = 0;
|
|
427
|
+
for (const part of parts) {
|
|
428
|
+
result.set(part, offset);
|
|
429
|
+
offset += part.length;
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
function parseHybridKeyMaterial(data) {
|
|
434
|
+
let offset = 0;
|
|
435
|
+
const x25519EphemeralPk = data.slice(offset, offset + 32);
|
|
436
|
+
offset += 32;
|
|
437
|
+
const x25519Nonce = data.slice(offset, offset + 24);
|
|
438
|
+
offset += 24;
|
|
439
|
+
const x25519WrappedLen = readUint16BE(data, offset);
|
|
440
|
+
offset += 2;
|
|
441
|
+
const x25519WrappedKey = data.slice(offset, offset + x25519WrappedLen);
|
|
442
|
+
offset += x25519WrappedLen;
|
|
443
|
+
const kyberCtLen = readUint16BE(data, offset);
|
|
444
|
+
offset += 2;
|
|
445
|
+
const kyberCiphertext = data.slice(offset, offset + kyberCtLen);
|
|
446
|
+
offset += kyberCtLen;
|
|
447
|
+
const kyberNonce = data.slice(offset, offset + 24);
|
|
448
|
+
offset += 24;
|
|
449
|
+
const kyberWrappedLen = readUint16BE(data, offset);
|
|
450
|
+
offset += 2;
|
|
451
|
+
const kyberWrappedKey = data.slice(offset, offset + kyberWrappedLen);
|
|
452
|
+
return {
|
|
453
|
+
x25519EphemeralPk,
|
|
454
|
+
x25519Nonce,
|
|
455
|
+
x25519WrappedKey,
|
|
456
|
+
kyberCiphertext,
|
|
457
|
+
kyberNonce,
|
|
458
|
+
kyberWrappedKey
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function serializePasswordKeyMaterial(km) {
|
|
462
|
+
const result = new Uint8Array(32 + 4 + 4 + 4);
|
|
463
|
+
result.set(km.salt, 0);
|
|
464
|
+
result.set(writeUint32BE(km.memoryCost), 32);
|
|
465
|
+
result.set(writeUint32BE(km.timeCost), 36);
|
|
466
|
+
result.set(writeUint32BE(km.parallelism), 40);
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
function parsePasswordKeyMaterial(data) {
|
|
470
|
+
return {
|
|
471
|
+
salt: data.slice(0, 32),
|
|
472
|
+
memoryCost: readUint32BE(data, 32),
|
|
473
|
+
timeCost: readUint32BE(data, 36),
|
|
474
|
+
parallelism: readUint32BE(data, 40)
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function serializeMetadata(metadata) {
|
|
478
|
+
const json = JSON.stringify(metadata);
|
|
479
|
+
return textEncoder.encode(json);
|
|
480
|
+
}
|
|
481
|
+
function parseMetadata(data) {
|
|
482
|
+
const json = textDecoder.decode(data);
|
|
483
|
+
return JSON.parse(json);
|
|
484
|
+
}
|
|
485
|
+
function assembleOQEFile(components) {
|
|
486
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = components;
|
|
487
|
+
const headerBytes = writeOQEHeader(header);
|
|
488
|
+
const totalLength = headerBytes.length + keyMaterial.length + encryptedMetadata.length + encryptedContent.length;
|
|
489
|
+
const result = new Uint8Array(totalLength);
|
|
490
|
+
let offset = 0;
|
|
491
|
+
result.set(headerBytes, offset);
|
|
492
|
+
offset += headerBytes.length;
|
|
493
|
+
result.set(keyMaterial, offset);
|
|
494
|
+
offset += keyMaterial.length;
|
|
495
|
+
result.set(encryptedMetadata, offset);
|
|
496
|
+
offset += encryptedMetadata.length;
|
|
497
|
+
result.set(encryptedContent, offset);
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
function parseOQEFile(data) {
|
|
501
|
+
const header = parseOQEHeader(data);
|
|
502
|
+
let offset = OQE_HEADER_SIZE;
|
|
503
|
+
const keyMaterial = data.slice(offset, offset + header.keyMaterialLength);
|
|
504
|
+
offset += header.keyMaterialLength;
|
|
505
|
+
const encryptedMetadata = data.slice(offset, offset + header.metadataLength);
|
|
506
|
+
offset += header.metadataLength;
|
|
507
|
+
const encryptedContent = data.slice(offset);
|
|
508
|
+
return {
|
|
509
|
+
header,
|
|
510
|
+
keyMaterial,
|
|
511
|
+
encryptedMetadata,
|
|
512
|
+
encryptedContent
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
var OQE_EXTENSION = ".oqe";
|
|
516
|
+
function addOQEExtension(filename) {
|
|
517
|
+
if (filename.toLowerCase().endsWith(OQE_EXTENSION)) {
|
|
518
|
+
return filename;
|
|
519
|
+
}
|
|
520
|
+
return `${filename}${OQE_EXTENSION}`;
|
|
521
|
+
}
|
|
522
|
+
function removeOQEExtension(filename) {
|
|
523
|
+
if (filename.toLowerCase().endsWith(OQE_EXTENSION)) {
|
|
524
|
+
return filename.slice(0, -OQE_EXTENSION.length);
|
|
525
|
+
}
|
|
526
|
+
return filename;
|
|
527
|
+
}
|
|
528
|
+
function isOQEFile(filenameOrData) {
|
|
529
|
+
if (typeof filenameOrData === "string") {
|
|
530
|
+
return filenameOrData.toLowerCase().endsWith(OQE_EXTENSION);
|
|
531
|
+
}
|
|
532
|
+
if (filenameOrData.length < 4) return false;
|
|
533
|
+
return filenameOrData.slice(0, 4).every((b, i) => b === OQE_MAGIC[i]);
|
|
534
|
+
}
|
|
535
|
+
var OQE_MIME_TYPE = "application/x-omnituum-encrypted";
|
|
536
|
+
function getAlgorithmName(suiteId) {
|
|
537
|
+
if (suiteId === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
538
|
+
return "Hybrid (X25519 + Kyber768 + AES-256-GCM)";
|
|
539
|
+
}
|
|
540
|
+
if (suiteId === ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
541
|
+
return "Password (Argon2id + AES-256-GCM)";
|
|
542
|
+
}
|
|
543
|
+
return `Unknown (0x${suiteId.toString(16)})`;
|
|
544
|
+
}
|
|
545
|
+
var kyberModule = null;
|
|
546
|
+
async function loadKyber() {
|
|
547
|
+
if (kyberModule) return kyberModule;
|
|
548
|
+
try {
|
|
549
|
+
const m = await import('kyber-crystals');
|
|
550
|
+
const k = m.default ?? m;
|
|
551
|
+
kyberModule = k.kyber ?? k;
|
|
552
|
+
return kyberModule;
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.warn("[Kyber] Failed to load kyber-crystals:", e);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function isKyberAvailable() {
|
|
559
|
+
const mod = await loadKyber();
|
|
560
|
+
return mod !== null;
|
|
561
|
+
}
|
|
562
|
+
async function kyberEncapsulate(pubKeyB64) {
|
|
563
|
+
const kyber = await loadKyber();
|
|
564
|
+
if (!kyber?.encrypt) {
|
|
565
|
+
throw new Error("Kyber encrypt not available");
|
|
566
|
+
}
|
|
567
|
+
const pk = ub64(pubKeyB64);
|
|
568
|
+
const r = await kyber.encrypt(pk);
|
|
569
|
+
const ctRaw = r?.ciphertext ?? r?.cyphertext ?? r?.ct ?? r?.bytes?.ciphertext ?? r?.bytes?.cyphertext ?? r?.bytes?.ct ?? (Array.isArray(r) ? r[0] : void 0);
|
|
570
|
+
const ssRaw = r?.key ?? r?.sharedSecret ?? r?.secret ?? r?.bytes?.key ?? r?.bytes?.sharedSecret ?? (Array.isArray(r) ? r[1] : void 0);
|
|
571
|
+
if (!ctRaw || !ssRaw) {
|
|
572
|
+
throw new Error("Kyber encapsulate failed: missing ciphertext or shared secret");
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
ciphertext: new Uint8Array(ctRaw),
|
|
576
|
+
sharedSecret: new Uint8Array(ssRaw)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function kyberDecapsulate(kemCiphertextB64, secretKeyB64) {
|
|
580
|
+
const kyber = await loadKyber();
|
|
581
|
+
if (!kyber?.decrypt && !kyber?.decapsulate) {
|
|
582
|
+
throw new Error("Kyber decrypt/decapsulate not available");
|
|
583
|
+
}
|
|
584
|
+
const ct = ub64(kemCiphertextB64);
|
|
585
|
+
const sk = ub64(secretKeyB64);
|
|
586
|
+
const r = kyber.decrypt ? await kyber.decrypt(ct, sk) : await kyber.decapsulate(ct, sk);
|
|
587
|
+
const key = r && (r.key ?? r.sharedSecret) ? r.key ?? r.sharedSecret : r;
|
|
588
|
+
return new Uint8Array(key);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/fs/encrypt.ts
|
|
592
|
+
function hkdfFlex(ikm, salt, info) {
|
|
593
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
594
|
+
}
|
|
595
|
+
function computeIdentityHash(publicKeyHex) {
|
|
596
|
+
const hash = sha256(fromHex(publicKeyHex));
|
|
597
|
+
return toHex(hash).slice(0, 16);
|
|
598
|
+
}
|
|
599
|
+
async function encryptHybrid(plaintext, metadata, options) {
|
|
600
|
+
if (!await isKyberAvailable()) {
|
|
601
|
+
throw new OQEError("KYBER_UNAVAILABLE", "Kyber library not available in this environment");
|
|
602
|
+
}
|
|
603
|
+
const contentKey = rand32();
|
|
604
|
+
const iv = rand12();
|
|
605
|
+
const x25519EphKp = nacl2.box.keyPair();
|
|
606
|
+
const recipientX25519Pk = fromHex(options.recipientPublicKeys.x25519PubHex);
|
|
607
|
+
const x25519Shared = nacl2.scalarMult(x25519EphKp.secretKey, recipientX25519Pk);
|
|
608
|
+
const x25519Kek = hkdfFlex(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
609
|
+
const x25519Nonce = rand24();
|
|
610
|
+
const x25519WrappedKey = nacl2.secretbox(contentKey, x25519Nonce, x25519Kek);
|
|
611
|
+
const kyberResult = await kyberEncapsulate(options.recipientPublicKeys.kyberPubB64);
|
|
612
|
+
const kyberKek = hkdfFlex(kyberResult.sharedSecret, "omnituum/fs/kyber", "wrap-content-key");
|
|
613
|
+
const kyberNonce = rand24();
|
|
614
|
+
const kyberWrappedKey = nacl2.secretbox(contentKey, kyberNonce, kyberKek);
|
|
615
|
+
const keyMaterial = {
|
|
616
|
+
x25519EphemeralPk: x25519EphKp.publicKey,
|
|
617
|
+
x25519Nonce,
|
|
618
|
+
x25519WrappedKey,
|
|
619
|
+
kyberCiphertext: kyberResult.ciphertext,
|
|
620
|
+
kyberNonce,
|
|
621
|
+
kyberWrappedKey
|
|
622
|
+
};
|
|
623
|
+
const keyMaterialBytes = serializeHybridKeyMaterial(keyMaterial);
|
|
624
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
625
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
626
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
627
|
+
const header = {
|
|
628
|
+
version: OQE_FORMAT_VERSION,
|
|
629
|
+
algorithmSuite: ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM,
|
|
630
|
+
flags: 0,
|
|
631
|
+
metadataLength: encryptedMetadata.length,
|
|
632
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
633
|
+
iv
|
|
634
|
+
};
|
|
635
|
+
const fileData = assembleOQEFile({
|
|
636
|
+
header,
|
|
637
|
+
keyMaterial: keyMaterialBytes,
|
|
638
|
+
encryptedMetadata,
|
|
639
|
+
encryptedContent
|
|
640
|
+
});
|
|
641
|
+
return {
|
|
642
|
+
data: fileData,
|
|
643
|
+
filename: addOQEExtension(metadata.filename),
|
|
644
|
+
metadata,
|
|
645
|
+
mode: "hybrid"
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function encryptPassword(plaintext, metadata, options) {
|
|
649
|
+
if (!await isArgon2Available()) {
|
|
650
|
+
throw new OQEError("ARGON2_UNAVAILABLE", "Argon2 library not available in this environment");
|
|
651
|
+
}
|
|
652
|
+
const params = {
|
|
653
|
+
...DEFAULT_ARGON2ID_PARAMS,
|
|
654
|
+
...options.argon2Params
|
|
655
|
+
};
|
|
656
|
+
const salt = generateArgon2Salt(params.saltLength);
|
|
657
|
+
const contentKey = await deriveKeyFromPassword(options.password, salt, params);
|
|
658
|
+
const iv = rand12();
|
|
659
|
+
const keyMaterial = {
|
|
660
|
+
salt,
|
|
661
|
+
memoryCost: params.memoryCost,
|
|
662
|
+
timeCost: params.timeCost,
|
|
663
|
+
parallelism: params.parallelism
|
|
664
|
+
};
|
|
665
|
+
const keyMaterialBytes = serializePasswordKeyMaterial(keyMaterial);
|
|
666
|
+
const metadataBytes = serializeMetadata(metadata);
|
|
667
|
+
const { ciphertext: encryptedMetadata } = await aesEncrypt(metadataBytes, contentKey, iv);
|
|
668
|
+
const { ciphertext: encryptedContent } = await aesEncrypt(plaintext, contentKey, iv);
|
|
669
|
+
const header = {
|
|
670
|
+
version: OQE_FORMAT_VERSION,
|
|
671
|
+
algorithmSuite: ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM,
|
|
672
|
+
flags: 0,
|
|
673
|
+
metadataLength: encryptedMetadata.length,
|
|
674
|
+
keyMaterialLength: keyMaterialBytes.length,
|
|
675
|
+
iv
|
|
676
|
+
};
|
|
677
|
+
const fileData = assembleOQEFile({
|
|
678
|
+
header,
|
|
679
|
+
keyMaterial: keyMaterialBytes,
|
|
680
|
+
encryptedMetadata,
|
|
681
|
+
encryptedContent
|
|
682
|
+
});
|
|
683
|
+
return {
|
|
684
|
+
data: fileData,
|
|
685
|
+
filename: addOQEExtension(metadata.filename),
|
|
686
|
+
metadata,
|
|
687
|
+
mode: "password"
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async function encryptFile(input, options) {
|
|
691
|
+
const plaintext = await toUint8Array(input.data);
|
|
692
|
+
const metadata = {
|
|
693
|
+
filename: input.filename,
|
|
694
|
+
originalSize: plaintext.length,
|
|
695
|
+
mimeType: input.mimeType,
|
|
696
|
+
encryptedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
697
|
+
};
|
|
698
|
+
if (options.mode === "hybrid") {
|
|
699
|
+
metadata.recipientIdHash = computeIdentityHash(options.recipientPublicKeys.x25519PubHex);
|
|
700
|
+
if (options.sender) {
|
|
701
|
+
metadata.encryptorIdHash = options.sender.id;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (options.mode === "hybrid") {
|
|
705
|
+
return encryptHybrid(plaintext, metadata, options);
|
|
706
|
+
} else {
|
|
707
|
+
return encryptPassword(plaintext, metadata, options);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function encryptFileForSelf(input, identity) {
|
|
711
|
+
return encryptFile(input, {
|
|
712
|
+
mode: "hybrid",
|
|
713
|
+
recipientPublicKeys: {
|
|
714
|
+
x25519PubHex: identity.x25519PubHex,
|
|
715
|
+
kyberPubB64: identity.kyberPubB64
|
|
716
|
+
},
|
|
717
|
+
sender: {
|
|
718
|
+
id: identity.id,
|
|
719
|
+
name: identity.name
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
async function encryptFileWithPassword(input, password) {
|
|
724
|
+
return encryptFile(input, {
|
|
725
|
+
mode: "password",
|
|
726
|
+
password
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
function hkdfFlex2(ikm, salt, info) {
|
|
730
|
+
return hkdfSha256(ikm, { salt: u8(salt), info: u8(info), length: 32 });
|
|
731
|
+
}
|
|
732
|
+
async function decryptHybrid(encryptedData, options) {
|
|
733
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
734
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
735
|
+
throw new OQEError(
|
|
736
|
+
"UNSUPPORTED_ALGORITHM",
|
|
737
|
+
"This file was not encrypted with hybrid mode. Use password decryption."
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const km = parseHybridKeyMaterial(keyMaterial);
|
|
741
|
+
let contentKey = null;
|
|
742
|
+
if (await isKyberAvailable()) {
|
|
743
|
+
try {
|
|
744
|
+
const kyberShared = await kyberDecapsulate(
|
|
745
|
+
toB64(km.kyberCiphertext),
|
|
746
|
+
options.recipientSecretKeys.kyberSecB64
|
|
747
|
+
);
|
|
748
|
+
const kyberKek = hkdfFlex2(kyberShared, "omnituum/fs/kyber", "wrap-content-key");
|
|
749
|
+
const unwrapped = nacl2.secretbox.open(km.kyberWrappedKey, km.kyberNonce, kyberKek);
|
|
750
|
+
if (unwrapped) {
|
|
751
|
+
contentKey = unwrapped;
|
|
752
|
+
console.log("[OQE] Decrypted content key via Kyber (post-quantum secure)");
|
|
753
|
+
}
|
|
754
|
+
} catch (e) {
|
|
755
|
+
console.warn("[OQE] Kyber decapsulation failed, trying X25519:", e);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (!contentKey) {
|
|
759
|
+
try {
|
|
760
|
+
const ephPk = km.x25519EphemeralPk;
|
|
761
|
+
const sk = fromHex(options.recipientSecretKeys.x25519SecHex);
|
|
762
|
+
const x25519Shared = nacl2.scalarMult(sk, ephPk);
|
|
763
|
+
const x25519Kek = hkdfFlex2(x25519Shared, "omnituum/fs/x25519", "wrap-content-key");
|
|
764
|
+
const unwrapped = nacl2.secretbox.open(km.x25519WrappedKey, km.x25519Nonce, x25519Kek);
|
|
765
|
+
if (unwrapped) {
|
|
766
|
+
contentKey = unwrapped;
|
|
767
|
+
console.log("[OQE] Decrypted content key via X25519 (classical)");
|
|
768
|
+
}
|
|
769
|
+
} catch (e) {
|
|
770
|
+
console.warn("[OQE] X25519 decryption failed:", e);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!contentKey) {
|
|
774
|
+
throw new OQEError("KEY_UNWRAP_FAILED", "Could not unwrap content key with provided keys");
|
|
775
|
+
}
|
|
776
|
+
let metadata;
|
|
777
|
+
try {
|
|
778
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
779
|
+
metadata = parseMetadata(metadataBytes);
|
|
780
|
+
} catch (e) {
|
|
781
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file metadata");
|
|
782
|
+
}
|
|
783
|
+
let plaintext;
|
|
784
|
+
try {
|
|
785
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
786
|
+
} catch (e) {
|
|
787
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
data: plaintext,
|
|
791
|
+
filename: metadata.filename,
|
|
792
|
+
mimeType: metadata.mimeType,
|
|
793
|
+
originalSize: metadata.originalSize,
|
|
794
|
+
metadata,
|
|
795
|
+
mode: "hybrid"
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
async function decryptPassword(encryptedData, options) {
|
|
799
|
+
if (!await isArgon2Available()) {
|
|
800
|
+
throw new OQEError("ARGON2_UNAVAILABLE", "Argon2 library not available in this environment");
|
|
801
|
+
}
|
|
802
|
+
const { header, keyMaterial, encryptedMetadata, encryptedContent } = parseOQEFile(encryptedData);
|
|
803
|
+
if (header.algorithmSuite !== ALGORITHM_SUITES.PASSWORD_ARGON2ID_AES256GCM) {
|
|
804
|
+
throw new OQEError(
|
|
805
|
+
"UNSUPPORTED_ALGORITHM",
|
|
806
|
+
"This file was not encrypted with password mode. Use hybrid decryption."
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
const km = parsePasswordKeyMaterial(keyMaterial);
|
|
810
|
+
const contentKey = await deriveKeyFromPassword(options.password, km.salt, {
|
|
811
|
+
memoryCost: km.memoryCost,
|
|
812
|
+
timeCost: km.timeCost,
|
|
813
|
+
parallelism: km.parallelism,
|
|
814
|
+
hashLength: 32,
|
|
815
|
+
saltLength: km.salt.length
|
|
816
|
+
});
|
|
817
|
+
let metadata;
|
|
818
|
+
try {
|
|
819
|
+
const metadataBytes = await aesDecrypt(encryptedMetadata, contentKey, header.iv);
|
|
820
|
+
metadata = parseMetadata(metadataBytes);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
throw new OQEError("PASSWORD_WRONG", "Incorrect password or corrupted file");
|
|
823
|
+
}
|
|
824
|
+
let plaintext;
|
|
825
|
+
try {
|
|
826
|
+
plaintext = await aesDecrypt(encryptedContent, contentKey, header.iv);
|
|
827
|
+
} catch (e) {
|
|
828
|
+
throw new OQEError("DECRYPTION_FAILED", "Failed to decrypt file content");
|
|
829
|
+
}
|
|
830
|
+
return {
|
|
831
|
+
data: plaintext,
|
|
832
|
+
filename: metadata.filename,
|
|
833
|
+
mimeType: metadata.mimeType,
|
|
834
|
+
originalSize: metadata.originalSize,
|
|
835
|
+
metadata,
|
|
836
|
+
mode: "password"
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
async function decryptFile(encryptedData, options) {
|
|
840
|
+
const data = await toUint8Array(encryptedData);
|
|
841
|
+
if (options.mode === "hybrid") {
|
|
842
|
+
return decryptHybrid(data, options);
|
|
843
|
+
} else {
|
|
844
|
+
return decryptPassword(data, options);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function decryptFileForSelf(encryptedData, identity) {
|
|
848
|
+
return decryptFile(encryptedData, {
|
|
849
|
+
mode: "hybrid",
|
|
850
|
+
recipientSecretKeys: {
|
|
851
|
+
x25519SecHex: identity.x25519SecHex,
|
|
852
|
+
kyberSecB64: identity.kyberSecB64
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
async function decryptFileWithPassword(encryptedData, password) {
|
|
857
|
+
return decryptFile(encryptedData, {
|
|
858
|
+
mode: "password",
|
|
859
|
+
password
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
async function inspectOQEFile(data) {
|
|
863
|
+
const bytes = await toUint8Array(data);
|
|
864
|
+
const { header } = parseOQEFile(bytes);
|
|
865
|
+
let mode;
|
|
866
|
+
let algorithm;
|
|
867
|
+
if (header.algorithmSuite === ALGORITHM_SUITES.HYBRID_X25519_KYBER768_AES256GCM) {
|
|
868
|
+
mode = "hybrid";
|
|
869
|
+
algorithm = "X25519 + Kyber768 + AES-256-GCM";
|
|
870
|
+
} else {
|
|
871
|
+
mode = "password";
|
|
872
|
+
algorithm = "Argon2id + AES-256-GCM";
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
version: header.version,
|
|
876
|
+
mode,
|
|
877
|
+
algorithm,
|
|
878
|
+
supportsKyber: mode === "hybrid" && await isKyberAvailable(),
|
|
879
|
+
fileSize: bytes.length
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/fs/browser.ts
|
|
884
|
+
function toArrayBuffer2(data) {
|
|
885
|
+
if (data.buffer instanceof SharedArrayBuffer || data.byteOffset !== 0 || data.byteLength !== data.buffer.byteLength) {
|
|
886
|
+
const copy = new ArrayBuffer(data.byteLength);
|
|
887
|
+
new Uint8Array(copy).set(data);
|
|
888
|
+
return copy;
|
|
889
|
+
}
|
|
890
|
+
return data.buffer;
|
|
891
|
+
}
|
|
892
|
+
function downloadEncryptedFile(result) {
|
|
893
|
+
const blob = new Blob([toArrayBuffer2(result.data)], { type: OQE_MIME_TYPE });
|
|
894
|
+
downloadBlob(blob, result.filename);
|
|
895
|
+
}
|
|
896
|
+
function downloadDecryptedFile(result) {
|
|
897
|
+
const mimeType = result.mimeType || "application/octet-stream";
|
|
898
|
+
const blob = new Blob([toArrayBuffer2(result.data)], { type: mimeType });
|
|
899
|
+
downloadBlob(blob, result.filename);
|
|
900
|
+
}
|
|
901
|
+
function downloadBlob(blob, filename) {
|
|
902
|
+
const url = URL.createObjectURL(blob);
|
|
903
|
+
const link = document.createElement("a");
|
|
904
|
+
link.href = url;
|
|
905
|
+
link.download = filename;
|
|
906
|
+
link.style.display = "none";
|
|
907
|
+
document.body.appendChild(link);
|
|
908
|
+
link.click();
|
|
909
|
+
document.body.removeChild(link);
|
|
910
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
911
|
+
}
|
|
912
|
+
function downloadBytes(data, filename, mimeType) {
|
|
913
|
+
const blob = new Blob([toArrayBuffer2(data)], { type: mimeType || "application/octet-stream" });
|
|
914
|
+
downloadBlob(blob, filename);
|
|
915
|
+
}
|
|
916
|
+
async function readFile(file) {
|
|
917
|
+
const buffer = await file.arrayBuffer();
|
|
918
|
+
return new Uint8Array(buffer);
|
|
919
|
+
}
|
|
920
|
+
async function readFileAsText(file) {
|
|
921
|
+
return new Promise((resolve, reject) => {
|
|
922
|
+
const reader = new FileReader();
|
|
923
|
+
reader.onload = () => resolve(reader.result);
|
|
924
|
+
reader.onerror = () => reject(reader.error);
|
|
925
|
+
reader.readAsText(file);
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
async function readFileAsDataURL(file) {
|
|
929
|
+
return new Promise((resolve, reject) => {
|
|
930
|
+
const reader = new FileReader();
|
|
931
|
+
reader.onload = () => resolve(reader.result);
|
|
932
|
+
reader.onerror = () => reject(reader.error);
|
|
933
|
+
reader.readAsDataURL(file);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function createDropZone(options) {
|
|
937
|
+
const { element, onDrop, onDragEnter, onDragLeave, accept, multiple = true } = options;
|
|
938
|
+
let dragCounter = 0;
|
|
939
|
+
const handleDragEnter = (e) => {
|
|
940
|
+
e.preventDefault();
|
|
941
|
+
e.stopPropagation();
|
|
942
|
+
dragCounter++;
|
|
943
|
+
if (dragCounter === 1) {
|
|
944
|
+
onDragEnter?.();
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
const handleDragLeave = (e) => {
|
|
948
|
+
e.preventDefault();
|
|
949
|
+
e.stopPropagation();
|
|
950
|
+
dragCounter--;
|
|
951
|
+
if (dragCounter === 0) {
|
|
952
|
+
onDragLeave?.();
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
const handleDragOver = (e) => {
|
|
956
|
+
e.preventDefault();
|
|
957
|
+
e.stopPropagation();
|
|
958
|
+
};
|
|
959
|
+
const handleDrop = (e) => {
|
|
960
|
+
e.preventDefault();
|
|
961
|
+
e.stopPropagation();
|
|
962
|
+
dragCounter = 0;
|
|
963
|
+
onDragLeave?.();
|
|
964
|
+
const files = Array.from(e.dataTransfer?.files || []);
|
|
965
|
+
let filteredFiles = files;
|
|
966
|
+
if (accept && accept.length > 0) {
|
|
967
|
+
filteredFiles = files.filter((file) => {
|
|
968
|
+
return accept.some((pattern) => {
|
|
969
|
+
if (pattern.startsWith(".")) {
|
|
970
|
+
return file.name.toLowerCase().endsWith(pattern.toLowerCase());
|
|
971
|
+
}
|
|
972
|
+
if (pattern.endsWith("/*")) {
|
|
973
|
+
const type = pattern.slice(0, -2);
|
|
974
|
+
return file.type.startsWith(type);
|
|
975
|
+
}
|
|
976
|
+
return file.type === pattern;
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
if (!multiple && filteredFiles.length > 1) {
|
|
981
|
+
filteredFiles = [filteredFiles[0]];
|
|
982
|
+
}
|
|
983
|
+
if (filteredFiles.length > 0) {
|
|
984
|
+
onDrop(filteredFiles);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
element.addEventListener("dragenter", handleDragEnter);
|
|
988
|
+
element.addEventListener("dragleave", handleDragLeave);
|
|
989
|
+
element.addEventListener("dragover", handleDragOver);
|
|
990
|
+
element.addEventListener("drop", handleDrop);
|
|
991
|
+
return () => {
|
|
992
|
+
element.removeEventListener("dragenter", handleDragEnter);
|
|
993
|
+
element.removeEventListener("dragleave", handleDragLeave);
|
|
994
|
+
element.removeEventListener("dragover", handleDragOver);
|
|
995
|
+
element.removeEventListener("drop", handleDrop);
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function openFilePicker(options = {}) {
|
|
999
|
+
return new Promise((resolve) => {
|
|
1000
|
+
const input = document.createElement("input");
|
|
1001
|
+
input.type = "file";
|
|
1002
|
+
input.multiple = options.multiple ?? false;
|
|
1003
|
+
if (options.accept && options.accept.length > 0) {
|
|
1004
|
+
input.accept = options.accept.join(",");
|
|
1005
|
+
}
|
|
1006
|
+
input.onchange = () => {
|
|
1007
|
+
const files = Array.from(input.files || []);
|
|
1008
|
+
resolve(files);
|
|
1009
|
+
};
|
|
1010
|
+
input.oncancel = () => {
|
|
1011
|
+
resolve([]);
|
|
1012
|
+
};
|
|
1013
|
+
input.click();
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function openOQEFilePicker(multiple = false) {
|
|
1017
|
+
return openFilePicker({
|
|
1018
|
+
accept: [OQE_EXTENSION, OQE_MIME_TYPE],
|
|
1019
|
+
multiple
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function openFileToEncrypt(multiple = false) {
|
|
1023
|
+
return openFilePicker({ multiple });
|
|
1024
|
+
}
|
|
1025
|
+
function encryptResultToBlob(result) {
|
|
1026
|
+
return new Blob([toArrayBuffer2(result.data)], { type: OQE_MIME_TYPE });
|
|
1027
|
+
}
|
|
1028
|
+
function decryptResultToBlob(result) {
|
|
1029
|
+
const mimeType = result.mimeType || "application/octet-stream";
|
|
1030
|
+
return new Blob([toArrayBuffer2(result.data)], { type: mimeType });
|
|
1031
|
+
}
|
|
1032
|
+
function createObjectURL(blob) {
|
|
1033
|
+
return URL.createObjectURL(blob);
|
|
1034
|
+
}
|
|
1035
|
+
async function bytesToDataURL(data, mimeType) {
|
|
1036
|
+
const blob = new Blob([toArrayBuffer2(data)], { type: mimeType });
|
|
1037
|
+
return new Promise((resolve, reject) => {
|
|
1038
|
+
const reader = new FileReader();
|
|
1039
|
+
reader.onload = () => resolve(reader.result);
|
|
1040
|
+
reader.onerror = () => reject(reader.error);
|
|
1041
|
+
reader.readAsDataURL(blob);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
function getFileInfo(file) {
|
|
1045
|
+
return {
|
|
1046
|
+
name: file.name,
|
|
1047
|
+
size: file.size,
|
|
1048
|
+
type: file.type || "application/octet-stream",
|
|
1049
|
+
lastModified: file.lastModified,
|
|
1050
|
+
isOQE: isOQEFile(file.name),
|
|
1051
|
+
sizeFormatted: formatFileSize(file.size)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function formatFileSize(bytes) {
|
|
1055
|
+
if (bytes === 0) return "0 B";
|
|
1056
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
1057
|
+
const k = 1024;
|
|
1058
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1059
|
+
return `${(bytes / Math.pow(k, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
1060
|
+
}
|
|
1061
|
+
async function copyToClipboard(text) {
|
|
1062
|
+
try {
|
|
1063
|
+
await navigator.clipboard.writeText(text);
|
|
1064
|
+
return true;
|
|
1065
|
+
} catch {
|
|
1066
|
+
const textarea = document.createElement("textarea");
|
|
1067
|
+
textarea.value = text;
|
|
1068
|
+
textarea.style.position = "fixed";
|
|
1069
|
+
textarea.style.left = "-9999px";
|
|
1070
|
+
document.body.appendChild(textarea);
|
|
1071
|
+
textarea.select();
|
|
1072
|
+
const success = document.execCommand("copy");
|
|
1073
|
+
document.body.removeChild(textarea);
|
|
1074
|
+
return success;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function isBrowser() {
|
|
1078
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
1079
|
+
}
|
|
1080
|
+
function isWebCryptoAvailable() {
|
|
1081
|
+
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
1082
|
+
}
|
|
1083
|
+
function isFileAPIAvailable() {
|
|
1084
|
+
return typeof File !== "undefined" && typeof FileReader !== "undefined";
|
|
1085
|
+
}
|
|
1086
|
+
function isDragDropSupported() {
|
|
1087
|
+
const div = document.createElement("div");
|
|
1088
|
+
return "draggable" in div || "ondragstart" in div && "ondrop" in div;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
export { AES_GCM_IV_SIZE, AES_GCM_TAG_SIZE, AES_KEY_SIZE, ALGORITHM_SUITES, DEFAULT_ARGON2ID_PARAMS, MIN_ARGON2ID_PARAMS, OQEError, OQE_EXTENSION, OQE_FORMAT_VERSION, OQE_HEADER_SIZE, OQE_MAGIC, OQE_MIME_TYPE, STREAM_CHUNK_SIZE, addOQEExtension, aesDecrypt, aesDecryptCombined, aesDecryptStreaming, aesEncrypt, aesEncryptCombined, aesEncryptStreaming, assembleOQEFile, benchmarkArgon2, bytesToDataURL, copyToClipboard, createDropZone, createObjectURL, decryptFile, decryptFileForSelf, decryptFileWithPassword, decryptResultToBlob, deriveKeyFromPassword, downloadBlob, downloadBytes, downloadDecryptedFile, downloadEncryptedFile, encryptFile, encryptFileForSelf, encryptFileWithPassword, encryptResultToBlob, estimateArgon2Params, exportAesKey, formatFileSize, generateAesKey, generateArgon2Salt, getAlgorithmName, getFileInfo, importAesKey, inspectOQEFile, isArgon2Available, isBrowser, isDragDropSupported, isFileAPIAvailable, isOQEFile, isWebCryptoAvailable, openFilePicker, openFileToEncrypt, openOQEFilePicker, parseHybridKeyMaterial, parseMetadata, parseOQEFile, parseOQEHeader, parsePasswordKeyMaterial, readFile, readFileAsDataURL, readFileAsText, removeOQEExtension, serializeHybridKeyMaterial, serializeMetadata, serializePasswordKeyMaterial, toUint8Array, verifyPassword, writeOQEHeader };
|