@omnituum/pqc-shared 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +543 -0
- package/dist/crypto/index.cjs +807 -0
- package/dist/crypto/index.d.cts +641 -0
- package/dist/crypto/index.d.ts +641 -0
- package/dist/crypto/index.js +716 -0
- package/dist/decrypt-eSHlbh1j.d.cts +321 -0
- package/dist/decrypt-eSHlbh1j.d.ts +321 -0
- package/dist/fs/index.cjs +1168 -0
- package/dist/fs/index.d.cts +400 -0
- package/dist/fs/index.d.ts +400 -0
- package/dist/fs/index.js +1091 -0
- package/dist/index.cjs +2160 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +2031 -0
- package/dist/integrity-CCYjrap3.d.ts +31 -0
- package/dist/integrity-Dx9jukMH.d.cts +31 -0
- package/dist/types-61c7Q9ri.d.ts +134 -0
- package/dist/types-Ch0y-n7K.d.cts +134 -0
- package/dist/utils/index.cjs +129 -0
- package/dist/utils/index.d.cts +49 -0
- package/dist/utils/index.d.ts +49 -0
- package/dist/utils/index.js +114 -0
- package/dist/vault/index.cjs +713 -0
- package/dist/vault/index.d.cts +237 -0
- package/dist/vault/index.d.ts +237 -0
- package/dist/vault/index.js +677 -0
- package/dist/version-BygzPVGs.d.cts +55 -0
- package/dist/version-BygzPVGs.d.ts +55 -0
- package/package.json +86 -0
- package/src/crypto/dilithium.ts +233 -0
- package/src/crypto/hybrid.ts +358 -0
- package/src/crypto/index.ts +181 -0
- package/src/crypto/kyber.ts +199 -0
- package/src/crypto/nacl.ts +204 -0
- package/src/crypto/primitives/blake3.ts +141 -0
- package/src/crypto/primitives/chacha.ts +211 -0
- package/src/crypto/primitives/hkdf.ts +192 -0
- package/src/crypto/primitives/index.ts +54 -0
- package/src/crypto/primitives.ts +144 -0
- package/src/crypto/x25519.ts +134 -0
- package/src/fs/aes.ts +343 -0
- package/src/fs/argon2.ts +184 -0
- package/src/fs/browser.ts +408 -0
- package/src/fs/decrypt.ts +320 -0
- package/src/fs/encrypt.ts +324 -0
- package/src/fs/format.ts +425 -0
- package/src/fs/index.ts +144 -0
- package/src/fs/types.ts +304 -0
- package/src/index.ts +414 -0
- package/src/kdf/index.ts +311 -0
- package/src/runtime/crypto.ts +16 -0
- package/src/security/index.ts +345 -0
- package/src/tunnel/index.ts +39 -0
- package/src/tunnel/session.ts +229 -0
- package/src/tunnel/types.ts +115 -0
- package/src/utils/entropy.ts +128 -0
- package/src/utils/index.ts +25 -0
- package/src/utils/integrity.ts +95 -0
- package/src/vault/decrypt.ts +167 -0
- package/src/vault/encrypt.ts +207 -0
- package/src/vault/index.ts +71 -0
- package/src/vault/manager.ts +327 -0
- package/src/vault/migrate.ts +190 -0
- package/src/vault/types.ts +177 -0
- package/src/version.ts +304 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import '@noble/hashes/sha256';
|
|
2
|
+
import '@noble/hashes/hmac';
|
|
3
|
+
import { argon2id } from 'hash-wasm';
|
|
4
|
+
import nacl from 'tweetnacl';
|
|
5
|
+
import '@noble/hashes/blake3';
|
|
6
|
+
import '@noble/ciphers/chacha';
|
|
7
|
+
import '@noble/hashes/hkdf';
|
|
8
|
+
import '@noble/hashes/sha512';
|
|
9
|
+
|
|
10
|
+
// src/vault/types.ts
|
|
11
|
+
var DEFAULT_VAULT_SETTINGS = {
|
|
12
|
+
autoUnlock: false,
|
|
13
|
+
lockTimeout: 15,
|
|
14
|
+
showFingerprints: true
|
|
15
|
+
};
|
|
16
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
17
|
+
new TextEncoder();
|
|
18
|
+
new TextDecoder();
|
|
19
|
+
function toB64(bytes) {
|
|
20
|
+
let binary = "";
|
|
21
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
22
|
+
binary += String.fromCharCode(bytes[i]);
|
|
23
|
+
}
|
|
24
|
+
return btoa(binary);
|
|
25
|
+
}
|
|
26
|
+
function fromB64(str) {
|
|
27
|
+
const binary = atob(str);
|
|
28
|
+
const bytes = new Uint8Array(binary.length);
|
|
29
|
+
for (let i = 0; i < binary.length; i++) {
|
|
30
|
+
bytes[i] = binary.charCodeAt(i);
|
|
31
|
+
}
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
function toHex(bytes) {
|
|
35
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
36
|
+
}
|
|
37
|
+
var b64 = toB64;
|
|
38
|
+
|
|
39
|
+
// src/version.ts
|
|
40
|
+
var VAULT_VERSION = "omnituum.vault.v1";
|
|
41
|
+
var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
|
|
42
|
+
var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
|
|
43
|
+
var VAULT_KDF = "PBKDF2-SHA256";
|
|
44
|
+
var VAULT_KDF_V2 = "Argon2id";
|
|
45
|
+
var VAULT_ALGORITHM = "AES-256-GCM";
|
|
46
|
+
var SUPPORTED_VAULT_VERSIONS = [
|
|
47
|
+
VAULT_VERSION
|
|
48
|
+
];
|
|
49
|
+
var SUPPORTED_VAULT_ENCRYPTED_VERSIONS = [
|
|
50
|
+
VAULT_ENCRYPTED_VERSION,
|
|
51
|
+
VAULT_ENCRYPTED_VERSION_V2
|
|
52
|
+
];
|
|
53
|
+
var VersionMismatchError = class extends Error {
|
|
54
|
+
constructor(type, expected, received) {
|
|
55
|
+
super(
|
|
56
|
+
`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.`
|
|
57
|
+
);
|
|
58
|
+
this.type = type;
|
|
59
|
+
this.expected = expected;
|
|
60
|
+
this.received = received;
|
|
61
|
+
this.name = "VersionMismatchError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function assertVaultVersion(version) {
|
|
65
|
+
if (!SUPPORTED_VAULT_VERSIONS.includes(version)) {
|
|
66
|
+
throw new VersionMismatchError("vault", SUPPORTED_VAULT_VERSIONS, version);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function assertVaultEncryptedVersion(version) {
|
|
70
|
+
if (!SUPPORTED_VAULT_ENCRYPTED_VERSIONS.includes(version)) {
|
|
71
|
+
throw new VersionMismatchError("vault_encrypted", SUPPORTED_VAULT_ENCRYPTED_VERSIONS, version);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var KDF_CONFIG_PBKDF2 = {
|
|
75
|
+
algorithm: "PBKDF2-SHA256",
|
|
76
|
+
pbkdf2Iterations: 6e5,
|
|
77
|
+
// OWASP 2023
|
|
78
|
+
saltLength: 32,
|
|
79
|
+
hashLength: 32
|
|
80
|
+
};
|
|
81
|
+
var KDF_CONFIG_ARGON2ID = {
|
|
82
|
+
algorithm: "Argon2id",
|
|
83
|
+
argon2MemoryCost: 65536,
|
|
84
|
+
// 64 MB
|
|
85
|
+
argon2TimeCost: 3,
|
|
86
|
+
argon2Parallelism: 4,
|
|
87
|
+
saltLength: 32,
|
|
88
|
+
hashLength: 32
|
|
89
|
+
};
|
|
90
|
+
var KDF_CONFIG_DEFAULT = KDF_CONFIG_PBKDF2;
|
|
91
|
+
var textEncoder2 = new TextEncoder();
|
|
92
|
+
async function derivePBKDF2(password, salt, iterations, hashLength) {
|
|
93
|
+
const passwordKey = await globalThis.crypto.subtle.importKey(
|
|
94
|
+
"raw",
|
|
95
|
+
textEncoder2.encode(password),
|
|
96
|
+
"PBKDF2",
|
|
97
|
+
false,
|
|
98
|
+
["deriveBits"]
|
|
99
|
+
);
|
|
100
|
+
const saltBuffer = new ArrayBuffer(salt.length);
|
|
101
|
+
new Uint8Array(saltBuffer).set(salt);
|
|
102
|
+
const bits = await globalThis.crypto.subtle.deriveBits(
|
|
103
|
+
{
|
|
104
|
+
name: "PBKDF2",
|
|
105
|
+
salt: saltBuffer,
|
|
106
|
+
iterations,
|
|
107
|
+
hash: "SHA-256"
|
|
108
|
+
},
|
|
109
|
+
passwordKey,
|
|
110
|
+
hashLength * 8
|
|
111
|
+
);
|
|
112
|
+
return new Uint8Array(bits);
|
|
113
|
+
}
|
|
114
|
+
async function deriveArgon2id(password, salt, memoryCost, timeCost, parallelism, hashLength) {
|
|
115
|
+
const hash = await argon2id({
|
|
116
|
+
password,
|
|
117
|
+
salt,
|
|
118
|
+
parallelism,
|
|
119
|
+
iterations: timeCost,
|
|
120
|
+
memorySize: memoryCost,
|
|
121
|
+
hashLength,
|
|
122
|
+
outputType: "binary"
|
|
123
|
+
});
|
|
124
|
+
return new Uint8Array(hash);
|
|
125
|
+
}
|
|
126
|
+
async function kdfDeriveKey(password, salt, config = KDF_CONFIG_DEFAULT) {
|
|
127
|
+
if (salt.length !== config.saltLength) {
|
|
128
|
+
throw new Error(`Salt must be ${config.saltLength} bytes, got ${salt.length}`);
|
|
129
|
+
}
|
|
130
|
+
if (config.algorithm === "PBKDF2-SHA256") {
|
|
131
|
+
return derivePBKDF2(
|
|
132
|
+
password,
|
|
133
|
+
salt,
|
|
134
|
+
config.pbkdf2Iterations ?? 6e5,
|
|
135
|
+
config.hashLength
|
|
136
|
+
);
|
|
137
|
+
} else if (config.algorithm === "Argon2id") {
|
|
138
|
+
return deriveArgon2id(
|
|
139
|
+
password,
|
|
140
|
+
salt,
|
|
141
|
+
config.argon2MemoryCost ?? 65536,
|
|
142
|
+
config.argon2TimeCost ?? 3,
|
|
143
|
+
config.argon2Parallelism ?? 4,
|
|
144
|
+
config.hashLength
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error(`Unsupported KDF algorithm: ${config.algorithm}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function configFromParams(algorithm, params) {
|
|
151
|
+
{
|
|
152
|
+
return {
|
|
153
|
+
algorithm,
|
|
154
|
+
argon2MemoryCost: params.memoryCost,
|
|
155
|
+
argon2TimeCost: params.timeCost,
|
|
156
|
+
argon2Parallelism: params.parallelism,
|
|
157
|
+
saltLength: 32,
|
|
158
|
+
hashLength: 32
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/vault/encrypt.ts
|
|
164
|
+
var textEncoder3 = new TextEncoder();
|
|
165
|
+
async function deriveKey(password, salt, iterations = PBKDF2_ITERATIONS) {
|
|
166
|
+
const passwordKey = await globalThis.crypto.subtle.importKey(
|
|
167
|
+
"raw",
|
|
168
|
+
textEncoder3.encode(password),
|
|
169
|
+
"PBKDF2",
|
|
170
|
+
false,
|
|
171
|
+
["deriveBits", "deriveKey"]
|
|
172
|
+
);
|
|
173
|
+
const saltArrayBuffer = new ArrayBuffer(salt.length);
|
|
174
|
+
new Uint8Array(saltArrayBuffer).set(salt);
|
|
175
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
176
|
+
{
|
|
177
|
+
name: "PBKDF2",
|
|
178
|
+
salt: saltArrayBuffer,
|
|
179
|
+
iterations,
|
|
180
|
+
hash: "SHA-256"
|
|
181
|
+
},
|
|
182
|
+
passwordKey,
|
|
183
|
+
{ name: "AES-GCM", length: 256 },
|
|
184
|
+
false,
|
|
185
|
+
// not extractable
|
|
186
|
+
["encrypt", "decrypt"]
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
async function encryptVault(vault, password) {
|
|
190
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
191
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
192
|
+
const key = await deriveKey(password, salt);
|
|
193
|
+
const plaintext = textEncoder3.encode(JSON.stringify(vault));
|
|
194
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
195
|
+
{ name: "AES-GCM", iv },
|
|
196
|
+
key,
|
|
197
|
+
plaintext
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
version: VAULT_ENCRYPTED_VERSION,
|
|
201
|
+
kdf: VAULT_KDF,
|
|
202
|
+
iterations: PBKDF2_ITERATIONS,
|
|
203
|
+
salt: toB64(salt),
|
|
204
|
+
iv: toB64(iv),
|
|
205
|
+
ciphertext: toB64(new Uint8Array(ciphertext)),
|
|
206
|
+
algorithm: VAULT_ALGORITHM
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function encryptVaultToBlob(vault, password) {
|
|
210
|
+
const encrypted = await encryptVault(vault, password);
|
|
211
|
+
const json = JSON.stringify(encrypted, null, 2);
|
|
212
|
+
return new Blob([json], { type: "application/json" });
|
|
213
|
+
}
|
|
214
|
+
async function encryptVaultToDataURL(vault, password) {
|
|
215
|
+
const encrypted = await encryptVault(vault, password);
|
|
216
|
+
const json = JSON.stringify(encrypted, null, 2);
|
|
217
|
+
return "data:application/json;charset=utf-8," + encodeURIComponent(json);
|
|
218
|
+
}
|
|
219
|
+
async function encryptVaultV2(vault, password) {
|
|
220
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
221
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
222
|
+
const keyBytes = await kdfDeriveKey(password, salt, KDF_CONFIG_ARGON2ID);
|
|
223
|
+
const keyBuffer = new ArrayBuffer(keyBytes.length);
|
|
224
|
+
new Uint8Array(keyBuffer).set(keyBytes);
|
|
225
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
226
|
+
"raw",
|
|
227
|
+
keyBuffer,
|
|
228
|
+
{ name: "AES-GCM", length: 256 },
|
|
229
|
+
false,
|
|
230
|
+
["encrypt"]
|
|
231
|
+
);
|
|
232
|
+
const plaintext = textEncoder3.encode(JSON.stringify(vault));
|
|
233
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
234
|
+
{ name: "AES-GCM", iv },
|
|
235
|
+
key,
|
|
236
|
+
plaintext
|
|
237
|
+
);
|
|
238
|
+
return {
|
|
239
|
+
version: VAULT_ENCRYPTED_VERSION_V2,
|
|
240
|
+
kdf: VAULT_KDF_V2,
|
|
241
|
+
memoryCost: KDF_CONFIG_ARGON2ID.argon2MemoryCost,
|
|
242
|
+
timeCost: KDF_CONFIG_ARGON2ID.argon2TimeCost,
|
|
243
|
+
parallelism: KDF_CONFIG_ARGON2ID.argon2Parallelism,
|
|
244
|
+
salt: toB64(salt),
|
|
245
|
+
iv: toB64(iv),
|
|
246
|
+
ciphertext: toB64(new Uint8Array(ciphertext)),
|
|
247
|
+
algorithm: VAULT_ALGORITHM
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/vault/decrypt.ts
|
|
252
|
+
var textDecoder2 = new TextDecoder();
|
|
253
|
+
async function decryptVault(encryptedFile, password) {
|
|
254
|
+
assertVaultEncryptedVersion(encryptedFile.version);
|
|
255
|
+
const salt = fromB64(encryptedFile.salt);
|
|
256
|
+
const iv = fromB64(encryptedFile.iv);
|
|
257
|
+
const ciphertext = fromB64(encryptedFile.ciphertext);
|
|
258
|
+
let key;
|
|
259
|
+
if (encryptedFile.version === VAULT_ENCRYPTED_VERSION_V2) {
|
|
260
|
+
const v2File = encryptedFile;
|
|
261
|
+
const kdfConfig = configFromParams("Argon2id", {
|
|
262
|
+
memoryCost: v2File.memoryCost,
|
|
263
|
+
timeCost: v2File.timeCost,
|
|
264
|
+
parallelism: v2File.parallelism
|
|
265
|
+
});
|
|
266
|
+
const keyBytes = await kdfDeriveKey(password, salt, kdfConfig);
|
|
267
|
+
const keyBuffer = new ArrayBuffer(keyBytes.length);
|
|
268
|
+
new Uint8Array(keyBuffer).set(keyBytes);
|
|
269
|
+
key = await globalThis.crypto.subtle.importKey(
|
|
270
|
+
"raw",
|
|
271
|
+
keyBuffer,
|
|
272
|
+
{ name: "AES-GCM", length: 256 },
|
|
273
|
+
false,
|
|
274
|
+
["decrypt"]
|
|
275
|
+
);
|
|
276
|
+
} else {
|
|
277
|
+
key = await deriveKey(password, salt, encryptedFile.iterations);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const ivArrayBuffer = new ArrayBuffer(iv.length);
|
|
281
|
+
new Uint8Array(ivArrayBuffer).set(iv);
|
|
282
|
+
const ciphertextArrayBuffer = new ArrayBuffer(ciphertext.length);
|
|
283
|
+
new Uint8Array(ciphertextArrayBuffer).set(ciphertext);
|
|
284
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
285
|
+
{ name: "AES-GCM", iv: ivArrayBuffer },
|
|
286
|
+
key,
|
|
287
|
+
ciphertextArrayBuffer
|
|
288
|
+
);
|
|
289
|
+
const json = textDecoder2.decode(plaintext);
|
|
290
|
+
const vault = JSON.parse(json);
|
|
291
|
+
assertVaultVersion(vault.version);
|
|
292
|
+
if (!Array.isArray(vault.identities)) {
|
|
293
|
+
throw new Error("Invalid vault structure: missing identities array");
|
|
294
|
+
}
|
|
295
|
+
return vault;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof DOMException && error.name === "OperationError") {
|
|
298
|
+
throw new Error("Incorrect password or corrupted vault");
|
|
299
|
+
}
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function decryptVaultFromJson(json, password) {
|
|
304
|
+
const encryptedFile = JSON.parse(json);
|
|
305
|
+
return decryptVault(encryptedFile, password);
|
|
306
|
+
}
|
|
307
|
+
async function decryptVaultFromFile(file, password) {
|
|
308
|
+
const text = await file.text();
|
|
309
|
+
return decryptVaultFromJson(text, password);
|
|
310
|
+
}
|
|
311
|
+
function isValidEncryptedVaultFile(json) {
|
|
312
|
+
try {
|
|
313
|
+
const parsed = JSON.parse(json);
|
|
314
|
+
return parsed.version === VAULT_ENCRYPTED_VERSION && parsed.kdf === VAULT_KDF && typeof parsed.iterations === "number" && typeof parsed.salt === "string" && typeof parsed.iv === "string" && typeof parsed.ciphertext === "string" && parsed.algorithm === VAULT_ALGORITHM;
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/utils/integrity.ts
|
|
321
|
+
function computeIntegrityHash(identities) {
|
|
322
|
+
const canonical = identities.map((i) => ({
|
|
323
|
+
id: i.id,
|
|
324
|
+
name: i.name,
|
|
325
|
+
x25519PubHex: i.x25519PubHex,
|
|
326
|
+
kyberPubB64: i.kyberPubB64,
|
|
327
|
+
createdAt: i.createdAt,
|
|
328
|
+
rotationCount: i.rotationCount
|
|
329
|
+
}));
|
|
330
|
+
const serialized = JSON.stringify(canonical, Object.keys(canonical[0] || {}).sort());
|
|
331
|
+
return computeStringHash(serialized);
|
|
332
|
+
}
|
|
333
|
+
function computeStringHash(str) {
|
|
334
|
+
let hash = 0;
|
|
335
|
+
for (let i = 0; i < str.length; i++) {
|
|
336
|
+
const char = str.charCodeAt(i);
|
|
337
|
+
hash = (hash << 5) - hash + char;
|
|
338
|
+
hash = hash & hash;
|
|
339
|
+
}
|
|
340
|
+
return Math.abs(hash).toString(16).padStart(16, "0");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/utils/entropy.ts
|
|
344
|
+
function generateId() {
|
|
345
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
346
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
347
|
+
}
|
|
348
|
+
nacl.secretbox.keyLength;
|
|
349
|
+
nacl.secretbox.nonceLength;
|
|
350
|
+
nacl.secretbox.overheadLength;
|
|
351
|
+
nacl.box.publicKeyLength;
|
|
352
|
+
nacl.box.nonceLength;
|
|
353
|
+
function generateX25519Keypair() {
|
|
354
|
+
const kp = nacl.box.keyPair();
|
|
355
|
+
return {
|
|
356
|
+
publicHex: "0x" + toHex(kp.publicKey),
|
|
357
|
+
secretHex: "0x" + toHex(kp.secretKey),
|
|
358
|
+
publicBytes: kp.publicKey,
|
|
359
|
+
secretBytes: kp.secretKey
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
var kyberModule = null;
|
|
363
|
+
async function loadKyber() {
|
|
364
|
+
if (kyberModule) return kyberModule;
|
|
365
|
+
try {
|
|
366
|
+
const m = await import('kyber-crystals');
|
|
367
|
+
const k = m.default ?? m;
|
|
368
|
+
kyberModule = k.kyber ?? k;
|
|
369
|
+
return kyberModule;
|
|
370
|
+
} catch (e) {
|
|
371
|
+
console.warn("[Kyber] Failed to load kyber-crystals:", e);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function generateKyberKeypair() {
|
|
376
|
+
try {
|
|
377
|
+
const mod = await loadKyber();
|
|
378
|
+
if (!mod) return null;
|
|
379
|
+
if (mod?.ready?.then) {
|
|
380
|
+
await mod.ready;
|
|
381
|
+
}
|
|
382
|
+
const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
|
|
383
|
+
if (typeof fn !== "function") {
|
|
384
|
+
console.warn("[Kyber] No keypair function found");
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const kp = await fn.call(mod);
|
|
388
|
+
const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
|
|
389
|
+
const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
|
|
390
|
+
if (!pub || !priv) {
|
|
391
|
+
console.warn("[Kyber] Invalid keypair result");
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
publicB64: b64(new Uint8Array(pub)),
|
|
396
|
+
secretB64: b64(new Uint8Array(priv))
|
|
397
|
+
};
|
|
398
|
+
} catch (e) {
|
|
399
|
+
console.warn("[Kyber] Key generation failed:", e);
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/vault/manager.ts
|
|
405
|
+
function createEmptyVault() {
|
|
406
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
407
|
+
return {
|
|
408
|
+
version: VAULT_VERSION,
|
|
409
|
+
identities: [],
|
|
410
|
+
settings: {
|
|
411
|
+
autoUnlock: false,
|
|
412
|
+
lockTimeout: 15,
|
|
413
|
+
showFingerprints: true
|
|
414
|
+
},
|
|
415
|
+
integrityHash: computeIntegrityHash([]),
|
|
416
|
+
createdAt: now,
|
|
417
|
+
modifiedAt: now
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async function createIdentity(name) {
|
|
421
|
+
const x25519 = generateX25519Keypair();
|
|
422
|
+
const kyber = await generateKyberKeypair();
|
|
423
|
+
if (!kyber) {
|
|
424
|
+
console.error("Kyber key generation failed");
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
428
|
+
const deviceFingerprint = await getDeviceFingerprint();
|
|
429
|
+
return {
|
|
430
|
+
id: generateId(),
|
|
431
|
+
name,
|
|
432
|
+
x25519PubHex: x25519.publicHex,
|
|
433
|
+
x25519SecHex: x25519.secretHex,
|
|
434
|
+
kyberPubB64: kyber.publicB64,
|
|
435
|
+
kyberSecB64: kyber.secretB64,
|
|
436
|
+
createdAt: now,
|
|
437
|
+
rotationCount: 0,
|
|
438
|
+
deviceFingerprint
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function addIdentity(vault, identity) {
|
|
442
|
+
const identities = [...vault.identities, identity];
|
|
443
|
+
return {
|
|
444
|
+
...vault,
|
|
445
|
+
identities,
|
|
446
|
+
integrityHash: computeIntegrityHash(identities),
|
|
447
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function removeIdentity(vault, identityId) {
|
|
451
|
+
const identities = vault.identities.filter((i) => i.id !== identityId);
|
|
452
|
+
return {
|
|
453
|
+
...vault,
|
|
454
|
+
identities,
|
|
455
|
+
integrityHash: computeIntegrityHash(identities),
|
|
456
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
457
|
+
settings: {
|
|
458
|
+
...vault.settings,
|
|
459
|
+
lastUsedIdentity: vault.settings.lastUsedIdentity === identityId ? void 0 : vault.settings.lastUsedIdentity
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async function rotateIdentityKeys(vault, identityId) {
|
|
464
|
+
const index = vault.identities.findIndex((i) => i.id === identityId);
|
|
465
|
+
if (index === -1) return null;
|
|
466
|
+
const existing = vault.identities[index];
|
|
467
|
+
const x25519 = generateX25519Keypair();
|
|
468
|
+
const kyber = await generateKyberKeypair();
|
|
469
|
+
if (!kyber) return null;
|
|
470
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
471
|
+
const updated = {
|
|
472
|
+
...existing,
|
|
473
|
+
x25519PubHex: x25519.publicHex,
|
|
474
|
+
x25519SecHex: x25519.secretHex,
|
|
475
|
+
kyberPubB64: kyber.publicB64,
|
|
476
|
+
kyberSecB64: kyber.secretB64,
|
|
477
|
+
lastRotatedAt: now,
|
|
478
|
+
rotationCount: existing.rotationCount + 1
|
|
479
|
+
};
|
|
480
|
+
const identities = [...vault.identities];
|
|
481
|
+
identities[index] = updated;
|
|
482
|
+
return {
|
|
483
|
+
...vault,
|
|
484
|
+
identities,
|
|
485
|
+
integrityHash: computeIntegrityHash(identities),
|
|
486
|
+
modifiedAt: now
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function updateIdentityMetadata(vault, identityId, updates) {
|
|
490
|
+
const identities = vault.identities.map(
|
|
491
|
+
(i) => i.id === identityId ? { ...i, ...updates } : i
|
|
492
|
+
);
|
|
493
|
+
return {
|
|
494
|
+
...vault,
|
|
495
|
+
identities,
|
|
496
|
+
integrityHash: computeIntegrityHash(identities),
|
|
497
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function updateSettings(vault, settings) {
|
|
501
|
+
return {
|
|
502
|
+
...vault,
|
|
503
|
+
settings: { ...vault.settings, ...settings },
|
|
504
|
+
modifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function setActiveIdentity(vault, identityId) {
|
|
508
|
+
return updateSettings(vault, { lastUsedIdentity: identityId });
|
|
509
|
+
}
|
|
510
|
+
async function exportVault(vault, password) {
|
|
511
|
+
return encryptVaultToBlob(vault, password);
|
|
512
|
+
}
|
|
513
|
+
async function importVault(file, password) {
|
|
514
|
+
return decryptVaultFromFile(file, password);
|
|
515
|
+
}
|
|
516
|
+
async function downloadVault(vault, password) {
|
|
517
|
+
const blob = await exportVault(vault, password);
|
|
518
|
+
const url = URL.createObjectURL(blob);
|
|
519
|
+
const a = document.createElement("a");
|
|
520
|
+
a.href = url;
|
|
521
|
+
a.download = `omnituum_vault_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.enc`;
|
|
522
|
+
document.body.appendChild(a);
|
|
523
|
+
a.click();
|
|
524
|
+
document.body.removeChild(a);
|
|
525
|
+
URL.revokeObjectURL(url);
|
|
526
|
+
}
|
|
527
|
+
var currentSession = {
|
|
528
|
+
unlocked: false,
|
|
529
|
+
sessionKey: null,
|
|
530
|
+
unlockedAt: null,
|
|
531
|
+
activeIdentityId: null
|
|
532
|
+
};
|
|
533
|
+
function getSession() {
|
|
534
|
+
return { ...currentSession };
|
|
535
|
+
}
|
|
536
|
+
async function unlockSession(password, vault) {
|
|
537
|
+
try {
|
|
538
|
+
const salt = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
539
|
+
const passwordKey = await globalThis.crypto.subtle.importKey(
|
|
540
|
+
"raw",
|
|
541
|
+
new TextEncoder().encode(password),
|
|
542
|
+
"PBKDF2",
|
|
543
|
+
false,
|
|
544
|
+
["deriveBits", "deriveKey"]
|
|
545
|
+
);
|
|
546
|
+
const sessionKey = await globalThis.crypto.subtle.deriveKey(
|
|
547
|
+
{
|
|
548
|
+
name: "PBKDF2",
|
|
549
|
+
salt,
|
|
550
|
+
iterations: 1e5,
|
|
551
|
+
// Faster for session key
|
|
552
|
+
hash: "SHA-256"
|
|
553
|
+
},
|
|
554
|
+
passwordKey,
|
|
555
|
+
{ name: "AES-GCM", length: 256 },
|
|
556
|
+
false,
|
|
557
|
+
["encrypt", "decrypt"]
|
|
558
|
+
);
|
|
559
|
+
currentSession = {
|
|
560
|
+
unlocked: true,
|
|
561
|
+
sessionKey,
|
|
562
|
+
unlockedAt: Date.now(),
|
|
563
|
+
activeIdentityId: vault.settings.lastUsedIdentity || vault.identities[0]?.id || null
|
|
564
|
+
};
|
|
565
|
+
return true;
|
|
566
|
+
} catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function lockSession() {
|
|
571
|
+
currentSession = {
|
|
572
|
+
unlocked: false,
|
|
573
|
+
sessionKey: null,
|
|
574
|
+
unlockedAt: null,
|
|
575
|
+
activeIdentityId: null
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function setSessionActiveIdentity(identityId) {
|
|
579
|
+
if (currentSession.unlocked) {
|
|
580
|
+
currentSession.activeIdentityId = identityId;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function getDeviceFingerprint() {
|
|
584
|
+
const data = [
|
|
585
|
+
navigator.userAgent,
|
|
586
|
+
navigator.language,
|
|
587
|
+
screen.width,
|
|
588
|
+
screen.height,
|
|
589
|
+
(/* @__PURE__ */ new Date()).getTimezoneOffset()
|
|
590
|
+
].join("|");
|
|
591
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
|
|
592
|
+
return toHex(new Uint8Array(hash)).slice(0, 16);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/security/index.ts
|
|
596
|
+
function zeroMemory(arr) {
|
|
597
|
+
if (!arr || arr.length === 0) return;
|
|
598
|
+
arr.fill(0);
|
|
599
|
+
if (arr[0] !== 0) {
|
|
600
|
+
throw new Error("Memory zeroing failed");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/vault/migrate.ts
|
|
605
|
+
function needsMigration(encryptedVault) {
|
|
606
|
+
return encryptedVault.version === VAULT_ENCRYPTED_VERSION;
|
|
607
|
+
}
|
|
608
|
+
function isV2Vault(encryptedVault) {
|
|
609
|
+
return encryptedVault.version === VAULT_ENCRYPTED_VERSION_V2;
|
|
610
|
+
}
|
|
611
|
+
function getVaultKdfInfo(encryptedVault) {
|
|
612
|
+
if (encryptedVault.version === VAULT_ENCRYPTED_VERSION_V2) {
|
|
613
|
+
const v2 = encryptedVault;
|
|
614
|
+
return {
|
|
615
|
+
kdf: `Argon2id (${v2.memoryCost / 1024}MB, ${v2.timeCost} iterations)`,
|
|
616
|
+
version: "v2",
|
|
617
|
+
isSecure: true
|
|
618
|
+
};
|
|
619
|
+
} else {
|
|
620
|
+
const v1 = encryptedVault;
|
|
621
|
+
return {
|
|
622
|
+
kdf: `PBKDF2-SHA256 (${v1.iterations.toLocaleString()} iterations)`,
|
|
623
|
+
version: "v1",
|
|
624
|
+
isSecure: true,
|
|
625
|
+
// Still secure, just not as modern
|
|
626
|
+
recommendation: "Consider upgrading to Argon2id for stronger protection"
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function migrateEncryptedVault(options) {
|
|
631
|
+
const { encryptedVault, password, keepBackup = false } = options;
|
|
632
|
+
if (isV2Vault(encryptedVault)) {
|
|
633
|
+
throw new Error("Vault is already v2 format, no migration needed");
|
|
634
|
+
}
|
|
635
|
+
let decryptedVaultJson = null;
|
|
636
|
+
try {
|
|
637
|
+
const vault = await decryptVault(encryptedVault, password);
|
|
638
|
+
const vaultJson = JSON.stringify(vault);
|
|
639
|
+
decryptedVaultJson = new TextEncoder().encode(vaultJson);
|
|
640
|
+
const newEncryptedVault = await encryptVaultV2(vault, password);
|
|
641
|
+
return {
|
|
642
|
+
encryptedVault: newEncryptedVault,
|
|
643
|
+
backup: keepBackup ? encryptedVault : void 0,
|
|
644
|
+
sourceVersion: encryptedVault.version,
|
|
645
|
+
targetVersion: VAULT_ENCRYPTED_VERSION_V2,
|
|
646
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
647
|
+
};
|
|
648
|
+
} finally {
|
|
649
|
+
if (decryptedVaultJson) {
|
|
650
|
+
zeroMemory(decryptedVaultJson);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function validateMigration(original, migrated, password) {
|
|
655
|
+
let originalVaultJson = null;
|
|
656
|
+
let migratedVaultJson = null;
|
|
657
|
+
try {
|
|
658
|
+
const originalVault = await decryptVault(original, password);
|
|
659
|
+
const migratedVault = await decryptVault(migrated, password);
|
|
660
|
+
originalVaultJson = new TextEncoder().encode(JSON.stringify(originalVault));
|
|
661
|
+
migratedVaultJson = new TextEncoder().encode(JSON.stringify(migratedVault));
|
|
662
|
+
if (originalVaultJson.length !== migratedVaultJson.length) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
for (let i = 0; i < originalVaultJson.length; i++) {
|
|
666
|
+
if (originalVaultJson[i] !== migratedVaultJson[i]) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
} finally {
|
|
672
|
+
if (originalVaultJson) zeroMemory(originalVaultJson);
|
|
673
|
+
if (migratedVaultJson) zeroMemory(migratedVaultJson);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export { DEFAULT_VAULT_SETTINGS, PBKDF2_ITERATIONS, addIdentity, createEmptyVault, createIdentity, decryptVault, decryptVaultFromFile, decryptVaultFromJson, deriveKey, downloadVault, encryptVault, encryptVaultToBlob, encryptVaultToDataURL, encryptVaultV2, exportVault, getSession, getVaultKdfInfo, importVault, isV2Vault, isValidEncryptedVaultFile, lockSession, migrateEncryptedVault, needsMigration, removeIdentity, rotateIdentityKeys, setActiveIdentity, setSessionActiveIdentity, unlockSession, updateIdentityMetadata, updateSettings, validateMigration };
|