@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.
Files changed (67) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +543 -0
  3. package/dist/crypto/index.cjs +807 -0
  4. package/dist/crypto/index.d.cts +641 -0
  5. package/dist/crypto/index.d.ts +641 -0
  6. package/dist/crypto/index.js +716 -0
  7. package/dist/decrypt-eSHlbh1j.d.cts +321 -0
  8. package/dist/decrypt-eSHlbh1j.d.ts +321 -0
  9. package/dist/fs/index.cjs +1168 -0
  10. package/dist/fs/index.d.cts +400 -0
  11. package/dist/fs/index.d.ts +400 -0
  12. package/dist/fs/index.js +1091 -0
  13. package/dist/index.cjs +2160 -0
  14. package/dist/index.d.cts +282 -0
  15. package/dist/index.d.ts +282 -0
  16. package/dist/index.js +2031 -0
  17. package/dist/integrity-CCYjrap3.d.ts +31 -0
  18. package/dist/integrity-Dx9jukMH.d.cts +31 -0
  19. package/dist/types-61c7Q9ri.d.ts +134 -0
  20. package/dist/types-Ch0y-n7K.d.cts +134 -0
  21. package/dist/utils/index.cjs +129 -0
  22. package/dist/utils/index.d.cts +49 -0
  23. package/dist/utils/index.d.ts +49 -0
  24. package/dist/utils/index.js +114 -0
  25. package/dist/vault/index.cjs +713 -0
  26. package/dist/vault/index.d.cts +237 -0
  27. package/dist/vault/index.d.ts +237 -0
  28. package/dist/vault/index.js +677 -0
  29. package/dist/version-BygzPVGs.d.cts +55 -0
  30. package/dist/version-BygzPVGs.d.ts +55 -0
  31. package/package.json +86 -0
  32. package/src/crypto/dilithium.ts +233 -0
  33. package/src/crypto/hybrid.ts +358 -0
  34. package/src/crypto/index.ts +181 -0
  35. package/src/crypto/kyber.ts +199 -0
  36. package/src/crypto/nacl.ts +204 -0
  37. package/src/crypto/primitives/blake3.ts +141 -0
  38. package/src/crypto/primitives/chacha.ts +211 -0
  39. package/src/crypto/primitives/hkdf.ts +192 -0
  40. package/src/crypto/primitives/index.ts +54 -0
  41. package/src/crypto/primitives.ts +144 -0
  42. package/src/crypto/x25519.ts +134 -0
  43. package/src/fs/aes.ts +343 -0
  44. package/src/fs/argon2.ts +184 -0
  45. package/src/fs/browser.ts +408 -0
  46. package/src/fs/decrypt.ts +320 -0
  47. package/src/fs/encrypt.ts +324 -0
  48. package/src/fs/format.ts +425 -0
  49. package/src/fs/index.ts +144 -0
  50. package/src/fs/types.ts +304 -0
  51. package/src/index.ts +414 -0
  52. package/src/kdf/index.ts +311 -0
  53. package/src/runtime/crypto.ts +16 -0
  54. package/src/security/index.ts +345 -0
  55. package/src/tunnel/index.ts +39 -0
  56. package/src/tunnel/session.ts +229 -0
  57. package/src/tunnel/types.ts +115 -0
  58. package/src/utils/entropy.ts +128 -0
  59. package/src/utils/index.ts +25 -0
  60. package/src/utils/integrity.ts +95 -0
  61. package/src/vault/decrypt.ts +167 -0
  62. package/src/vault/encrypt.ts +207 -0
  63. package/src/vault/index.ts +71 -0
  64. package/src/vault/manager.ts +327 -0
  65. package/src/vault/migrate.ts +190 -0
  66. package/src/vault/types.ts +177 -0
  67. package/src/version.ts +304 -0
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Omnituum PQC Shared - X25519 Key Exchange
3
+ *
4
+ * Browser-compatible X25519 operations using tweetnacl.
5
+ * Provides key generation, ECDH, and NaCl box operations.
6
+ */
7
+
8
+ import nacl from 'tweetnacl';
9
+ import { rand24, assertLen, toHex, fromHex, b64, ub64, sha256, hkdfSha256, u8 } from './primitives';
10
+
11
+ // ═══════════════════════════════════════════════════════════════════════════
12
+ // TYPES
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ export interface X25519Keypair {
16
+ publicKey: Uint8Array;
17
+ secretKey: Uint8Array;
18
+ }
19
+
20
+ export interface X25519KeypairHex {
21
+ publicHex: string;
22
+ secretHex: string;
23
+ publicBytes: Uint8Array;
24
+ secretBytes: Uint8Array;
25
+ }
26
+
27
+ export interface ClassicalWrap {
28
+ ephPubKey: string;
29
+ nonce: string;
30
+ boxed: string;
31
+ }
32
+
33
+ // ═══════════════════════════════════════════════════════════════════════════
34
+ // KEY GENERATION
35
+ // ═══════════════════════════════════════════════════════════════════════════
36
+
37
+ /**
38
+ * Generate a new random X25519 keypair.
39
+ */
40
+ export function generateX25519Keypair(): X25519KeypairHex {
41
+ const kp = nacl.box.keyPair();
42
+ return {
43
+ publicHex: '0x' + toHex(kp.publicKey),
44
+ secretHex: '0x' + toHex(kp.secretKey),
45
+ publicBytes: kp.publicKey,
46
+ secretBytes: kp.secretKey,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Generate a deterministic X25519 keypair from a 32-byte seed.
52
+ */
53
+ export function generateX25519KeypairFromSeed(seed: Uint8Array): X25519Keypair {
54
+ if (seed.length !== 32) {
55
+ throw new Error('X25519 seed must be 32 bytes');
56
+ }
57
+
58
+ // Hash seed for uniformity
59
+ const uniformSeed = sha256(seed);
60
+
61
+ // Generate keypair from secret key
62
+ const kp = nacl.box.keyPair.fromSecretKey(uniformSeed);
63
+
64
+ return {
65
+ publicKey: kp.publicKey,
66
+ secretKey: uniformSeed,
67
+ };
68
+ }
69
+
70
+ // ═══════════════════════════════════════════════════════════════════════════
71
+ // KEY WRAPPING
72
+ // ═══════════════════════════════════════════════════════════════════════════
73
+
74
+ /**
75
+ * Wrap a symmetric key for a recipient using X25519 ECDH.
76
+ * Creates an ephemeral keypair and encrypts the key using NaCl box.
77
+ */
78
+ export function boxWrapWithX25519(symKey32: Uint8Array, recipientPubKeyHex: string): ClassicalWrap {
79
+ assertLen('sym key', symKey32, 32);
80
+ const pk = fromHex(recipientPubKeyHex);
81
+ assertLen('recipient pubKey', pk, 32);
82
+
83
+ // Generate ephemeral keypair
84
+ const eph = nacl.box.keyPair();
85
+ const nonce = rand24();
86
+
87
+ // Encrypt the symmetric key
88
+ const boxed = nacl.box(symKey32, nonce, pk, eph.secretKey);
89
+
90
+ return {
91
+ ephPubKey: toHex(eph.publicKey),
92
+ nonce: b64(nonce),
93
+ boxed: b64(boxed),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Unwrap a symmetric key using X25519 ECDH.
99
+ */
100
+ export function boxUnwrapWithX25519(
101
+ wrap: ClassicalWrap,
102
+ recipientSecretKey32: Uint8Array
103
+ ): Uint8Array | null {
104
+ assertLen('recipient secretKey', recipientSecretKey32, 32);
105
+ const ephPk = fromHex(wrap.ephPubKey);
106
+ assertLen('ephemeral pubKey', ephPk, 32);
107
+
108
+ return nacl.box.open(
109
+ ub64(wrap.boxed),
110
+ ub64(wrap.nonce),
111
+ ephPk,
112
+ recipientSecretKey32
113
+ ) || null;
114
+ }
115
+
116
+ /**
117
+ * Perform raw X25519 ECDH to derive a shared secret.
118
+ */
119
+ export function x25519SharedSecret(
120
+ ourSecretKey: Uint8Array,
121
+ theirPublicKey: Uint8Array
122
+ ): Uint8Array {
123
+ assertLen('secret key', ourSecretKey, 32);
124
+ assertLen('public key', theirPublicKey, 32);
125
+ return nacl.scalarMult(ourSecretKey, theirPublicKey);
126
+ }
127
+
128
+ // ═══════════════════════════════════════════════════════════════════════════
129
+ // HKDF KEY DERIVATION HELPER
130
+ // ═══════════════════════════════════════════════════════════════════════════
131
+
132
+ export function deriveKeyFromShared(shared: Uint8Array, salt: string, info: string): Uint8Array {
133
+ return hkdfSha256(shared, { salt: u8(salt), info: u8(info), length: 32 });
134
+ }
package/src/fs/aes.ts ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Omnituum FS - AES-256-GCM Encryption
3
+ *
4
+ * File encryption using AES-256-GCM via Web Crypto API.
5
+ * Provides authenticated encryption with associated data (AEAD).
6
+ */
7
+
8
+ import { rand12 } from '../crypto/primitives';
9
+
10
+ // Helper to safely extract ArrayBuffer from Uint8Array (handles SharedArrayBuffer)
11
+ function toArrayBuffer(arr: Uint8Array): ArrayBuffer {
12
+ // If buffer is SharedArrayBuffer or view is offset, copy to new ArrayBuffer
13
+ if (arr.buffer instanceof SharedArrayBuffer || arr.byteOffset !== 0 || arr.byteLength !== arr.buffer.byteLength) {
14
+ const copy = new ArrayBuffer(arr.byteLength);
15
+ new Uint8Array(copy).set(arr);
16
+ return copy;
17
+ }
18
+ return arr.buffer as ArrayBuffer;
19
+ }
20
+
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+ // CONSTANTS
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ /** AES-256 key size in bytes */
26
+ export const AES_KEY_SIZE = 32;
27
+
28
+ /** AES-GCM IV size in bytes (96 bits recommended by NIST) */
29
+ export const AES_GCM_IV_SIZE = 12;
30
+
31
+ /** AES-GCM auth tag size in bytes (128 bits) */
32
+ export const AES_GCM_TAG_SIZE = 16;
33
+
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+ // KEY IMPORT
36
+ // ═══════════════════════════════════════════════════════════════════════════
37
+
38
+ /**
39
+ * Import a raw 256-bit key for AES-GCM operations.
40
+ *
41
+ * @param keyBytes - 32-byte raw key material
42
+ * @returns CryptoKey suitable for AES-GCM encryption/decryption
43
+ */
44
+ export async function importAesKey(keyBytes: Uint8Array): Promise<CryptoKey> {
45
+ if (keyBytes.length !== AES_KEY_SIZE) {
46
+ throw new Error(`AES key must be ${AES_KEY_SIZE} bytes, got ${keyBytes.length}`);
47
+ }
48
+
49
+ return globalThis.crypto.subtle.importKey(
50
+ 'raw',
51
+ toArrayBuffer(keyBytes),
52
+ { name: 'AES-GCM', length: 256 },
53
+ false, // not extractable
54
+ ['encrypt', 'decrypt']
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Generate a random 256-bit AES key.
60
+ *
61
+ * @returns CryptoKey for AES-GCM
62
+ */
63
+ export async function generateAesKey(): Promise<CryptoKey> {
64
+ return globalThis.crypto.subtle.generateKey(
65
+ { name: 'AES-GCM', length: 256 },
66
+ true, // extractable for wrapping
67
+ ['encrypt', 'decrypt']
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Export a CryptoKey to raw bytes.
73
+ *
74
+ * @param key - CryptoKey to export
75
+ * @returns 32-byte raw key
76
+ */
77
+ export async function exportAesKey(key: CryptoKey): Promise<Uint8Array> {
78
+ const exported = await globalThis.crypto.subtle.exportKey('raw', key);
79
+ return new Uint8Array(exported);
80
+ }
81
+
82
+ // ═══════════════════════════════════════════════════════════════════════════
83
+ // ENCRYPTION
84
+ // ═══════════════════════════════════════════════════════════════════════════
85
+
86
+ /**
87
+ * Encrypt data using AES-256-GCM.
88
+ *
89
+ * @param plaintext - Data to encrypt
90
+ * @param key - AES-256 key (CryptoKey or 32-byte Uint8Array)
91
+ * @param iv - Optional 12-byte IV (generated if not provided)
92
+ * @param additionalData - Optional additional authenticated data (AAD)
93
+ * @returns Object containing IV and ciphertext (with auth tag appended)
94
+ */
95
+ export async function aesEncrypt(
96
+ plaintext: Uint8Array,
97
+ key: CryptoKey | Uint8Array,
98
+ iv?: Uint8Array,
99
+ additionalData?: Uint8Array
100
+ ): Promise<{ iv: Uint8Array; ciphertext: Uint8Array }> {
101
+ // Import key if raw bytes provided
102
+ const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
103
+
104
+ // Generate IV if not provided
105
+ const ivBytes = iv ?? rand12();
106
+
107
+ if (ivBytes.length !== AES_GCM_IV_SIZE) {
108
+ throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${ivBytes.length}`);
109
+ }
110
+
111
+ // Encrypt with AES-GCM (convert Uint8Arrays to ArrayBuffer for Web Crypto compatibility)
112
+ const encrypted = await globalThis.crypto.subtle.encrypt(
113
+ {
114
+ name: 'AES-GCM',
115
+ iv: toArrayBuffer(ivBytes),
116
+ additionalData: additionalData ? toArrayBuffer(additionalData) : undefined,
117
+ tagLength: 128, // 16 bytes
118
+ },
119
+ cryptoKey,
120
+ toArrayBuffer(plaintext)
121
+ );
122
+
123
+ return {
124
+ iv: ivBytes,
125
+ ciphertext: new Uint8Array(encrypted), // Includes auth tag
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Encrypt data and return combined IV + ciphertext.
131
+ * Convenience method for simple encryption.
132
+ *
133
+ * @param plaintext - Data to encrypt
134
+ * @param key - AES-256 key
135
+ * @param additionalData - Optional AAD
136
+ * @returns Combined bytes: [IV (12 bytes)] [ciphertext + tag]
137
+ */
138
+ export async function aesEncryptCombined(
139
+ plaintext: Uint8Array,
140
+ key: CryptoKey | Uint8Array,
141
+ additionalData?: Uint8Array
142
+ ): Promise<Uint8Array> {
143
+ const { iv, ciphertext } = await aesEncrypt(plaintext, key, undefined, additionalData);
144
+
145
+ // Combine IV + ciphertext
146
+ const combined = new Uint8Array(iv.length + ciphertext.length);
147
+ combined.set(iv, 0);
148
+ combined.set(ciphertext, iv.length);
149
+
150
+ return combined;
151
+ }
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════
154
+ // DECRYPTION
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+
157
+ /**
158
+ * Decrypt data using AES-256-GCM.
159
+ *
160
+ * @param ciphertext - Encrypted data (with auth tag appended)
161
+ * @param key - AES-256 key (CryptoKey or 32-byte Uint8Array)
162
+ * @param iv - 12-byte IV used for encryption
163
+ * @param additionalData - Optional additional authenticated data (must match encryption)
164
+ * @returns Decrypted plaintext
165
+ * @throws Error if authentication fails (wrong key or tampered data)
166
+ */
167
+ export async function aesDecrypt(
168
+ ciphertext: Uint8Array,
169
+ key: CryptoKey | Uint8Array,
170
+ iv: Uint8Array,
171
+ additionalData?: Uint8Array
172
+ ): Promise<Uint8Array> {
173
+ // Import key if raw bytes provided
174
+ const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
175
+
176
+ if (iv.length !== AES_GCM_IV_SIZE) {
177
+ throw new Error(`IV must be ${AES_GCM_IV_SIZE} bytes, got ${iv.length}`);
178
+ }
179
+
180
+ try {
181
+ // Convert Uint8Arrays to ArrayBuffer for Web Crypto compatibility
182
+ const decrypted = await globalThis.crypto.subtle.decrypt(
183
+ {
184
+ name: 'AES-GCM',
185
+ iv: toArrayBuffer(iv),
186
+ additionalData: additionalData ? toArrayBuffer(additionalData) : undefined,
187
+ tagLength: 128,
188
+ },
189
+ cryptoKey,
190
+ toArrayBuffer(ciphertext)
191
+ );
192
+
193
+ return new Uint8Array(decrypted);
194
+ } catch (error) {
195
+ // Web Crypto throws a generic error for auth failures
196
+ throw new Error('Decryption failed: authentication tag mismatch (wrong key or corrupted data)');
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Decrypt combined IV + ciphertext.
202
+ * Convenience method for simple decryption.
203
+ *
204
+ * @param combined - Combined bytes: [IV (12 bytes)] [ciphertext + tag]
205
+ * @param key - AES-256 key
206
+ * @param additionalData - Optional AAD
207
+ * @returns Decrypted plaintext
208
+ */
209
+ export async function aesDecryptCombined(
210
+ combined: Uint8Array,
211
+ key: CryptoKey | Uint8Array,
212
+ additionalData?: Uint8Array
213
+ ): Promise<Uint8Array> {
214
+ if (combined.length < AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE) {
215
+ throw new Error(`Combined data too short: need at least ${AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE} bytes`);
216
+ }
217
+
218
+ const iv = combined.slice(0, AES_GCM_IV_SIZE);
219
+ const ciphertext = combined.slice(AES_GCM_IV_SIZE);
220
+
221
+ return aesDecrypt(ciphertext, key, iv, additionalData);
222
+ }
223
+
224
+ // ═══════════════════════════════════════════════════════════════════════════
225
+ // STREAMING ENCRYPTION (for large files)
226
+ // ═══════════════════════════════════════════════════════════════════════════
227
+
228
+ /** Chunk size for streaming operations (1 MB) */
229
+ export const STREAM_CHUNK_SIZE = 1024 * 1024;
230
+
231
+ /**
232
+ * Encrypt large data in chunks.
233
+ * Each chunk is encrypted with a unique IV derived from base IV + counter.
234
+ *
235
+ * Note: For files < 100MB, use regular aesEncrypt for simplicity.
236
+ * This is primarily for very large files where memory is a concern.
237
+ *
238
+ * @param plaintext - Data to encrypt
239
+ * @param key - AES-256 key
240
+ * @param onProgress - Optional progress callback
241
+ * @returns Encrypted data with format: [chunk count (4 bytes)] [IV] [chunks...]
242
+ */
243
+ export async function aesEncryptStreaming(
244
+ plaintext: Uint8Array,
245
+ key: CryptoKey | Uint8Array,
246
+ onProgress?: (percent: number) => void
247
+ ): Promise<Uint8Array> {
248
+ const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
249
+
250
+ const chunks: Uint8Array[] = [];
251
+ const totalChunks = Math.ceil(plaintext.length / STREAM_CHUNK_SIZE);
252
+
253
+ // Store chunk count (4 bytes, big-endian)
254
+ const header = new Uint8Array(4);
255
+ new DataView(header.buffer).setUint32(0, totalChunks, false);
256
+ chunks.push(header);
257
+
258
+ for (let i = 0; i < totalChunks; i++) {
259
+ const start = i * STREAM_CHUNK_SIZE;
260
+ const end = Math.min(start + STREAM_CHUNK_SIZE, plaintext.length);
261
+ const chunk = plaintext.slice(start, end);
262
+
263
+ // Encrypt chunk (IV is generated fresh for each chunk)
264
+ const { iv, ciphertext } = await aesEncrypt(chunk, cryptoKey);
265
+
266
+ // Store: [IV (12 bytes)] [ciphertext length (4 bytes)] [ciphertext]
267
+ const chunkData = new Uint8Array(12 + 4 + ciphertext.length);
268
+ chunkData.set(iv, 0);
269
+ new DataView(chunkData.buffer).setUint32(12, ciphertext.length, false);
270
+ chunkData.set(ciphertext, 16);
271
+ chunks.push(chunkData);
272
+
273
+ if (onProgress) {
274
+ onProgress(Math.round(((i + 1) / totalChunks) * 100));
275
+ }
276
+ }
277
+
278
+ // Combine all chunks
279
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
280
+ const result = new Uint8Array(totalLength);
281
+ let offset = 0;
282
+ for (const chunk of chunks) {
283
+ result.set(chunk, offset);
284
+ offset += chunk.length;
285
+ }
286
+
287
+ return result;
288
+ }
289
+
290
+ /**
291
+ * Decrypt large data encrypted with aesEncryptStreaming.
292
+ *
293
+ * @param encrypted - Encrypted data from aesEncryptStreaming
294
+ * @param key - AES-256 key
295
+ * @param onProgress - Optional progress callback
296
+ * @returns Decrypted plaintext
297
+ */
298
+ export async function aesDecryptStreaming(
299
+ encrypted: Uint8Array,
300
+ key: CryptoKey | Uint8Array,
301
+ onProgress?: (percent: number) => void
302
+ ): Promise<Uint8Array> {
303
+ const cryptoKey = key instanceof CryptoKey ? key : await importAesKey(key);
304
+
305
+ // Read chunk count
306
+ const totalChunks = new DataView(encrypted.buffer, encrypted.byteOffset).getUint32(0, false);
307
+ const chunks: Uint8Array[] = [];
308
+
309
+ let offset = 4; // Skip header
310
+
311
+ for (let i = 0; i < totalChunks; i++) {
312
+ // Read IV
313
+ const iv = encrypted.slice(offset, offset + 12);
314
+ offset += 12;
315
+
316
+ // Read ciphertext length
317
+ const ctLength = new DataView(encrypted.buffer, encrypted.byteOffset + offset).getUint32(0, false);
318
+ offset += 4;
319
+
320
+ // Read ciphertext
321
+ const ciphertext = encrypted.slice(offset, offset + ctLength);
322
+ offset += ctLength;
323
+
324
+ // Decrypt chunk
325
+ const plaintext = await aesDecrypt(ciphertext, cryptoKey, iv);
326
+ chunks.push(plaintext);
327
+
328
+ if (onProgress) {
329
+ onProgress(Math.round(((i + 1) / totalChunks) * 100));
330
+ }
331
+ }
332
+
333
+ // Combine all chunks
334
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
335
+ const result = new Uint8Array(totalLength);
336
+ let resultOffset = 0;
337
+ for (const chunk of chunks) {
338
+ result.set(chunk, resultOffset);
339
+ resultOffset += chunk.length;
340
+ }
341
+
342
+ return result;
343
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Omnituum FS - Argon2id Key Derivation
3
+ *
4
+ * Memory-hard password hashing using Argon2id (winner of PHC).
5
+ * Uses hash-wasm for browser-compatible WASM implementation.
6
+ *
7
+ * Security: OWASP 2024 recommended parameters.
8
+ */
9
+
10
+ import { argon2id } from 'hash-wasm';
11
+ import { randN } from '../crypto/primitives';
12
+ import {
13
+ Argon2idParams,
14
+ DEFAULT_ARGON2ID_PARAMS,
15
+ MIN_ARGON2ID_PARAMS,
16
+ } from './types';
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+ // ARGON2ID KEY DERIVATION
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+
22
+ /**
23
+ * Derive an encryption key from a password using Argon2id.
24
+ *
25
+ * @param password - User password
26
+ * @param salt - 32-byte random salt (generate with generateArgon2Salt())
27
+ * @param params - Argon2id parameters (uses OWASP defaults if not specified)
28
+ * @returns 32-byte derived key suitable for AES-256
29
+ */
30
+ export async function deriveKeyFromPassword(
31
+ password: string,
32
+ salt: Uint8Array,
33
+ params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
34
+ ): Promise<Uint8Array> {
35
+ if (salt.length !== params.saltLength) {
36
+ throw new Error(`Salt must be ${params.saltLength} bytes, got ${salt.length}`);
37
+ }
38
+
39
+ const hash = await argon2id({
40
+ password,
41
+ salt,
42
+ parallelism: params.parallelism,
43
+ iterations: params.timeCost,
44
+ memorySize: params.memoryCost,
45
+ hashLength: params.hashLength,
46
+ outputType: 'binary',
47
+ });
48
+
49
+ return new Uint8Array(hash);
50
+ }
51
+
52
+ /**
53
+ * Generate a random salt for Argon2id.
54
+ *
55
+ * @param length - Salt length in bytes (default: 32)
56
+ * @returns Random salt
57
+ */
58
+ export function generateArgon2Salt(length: number = 32): Uint8Array {
59
+ return randN(length);
60
+ }
61
+
62
+ /**
63
+ * Verify a password against a stored hash.
64
+ * Useful for vault unlocking or file password verification.
65
+ *
66
+ * @param password - Password to verify
67
+ * @param salt - Original salt used
68
+ * @param expectedKey - Expected derived key
69
+ * @param params - Argon2id parameters
70
+ * @returns true if password is correct
71
+ */
72
+ export async function verifyPassword(
73
+ password: string,
74
+ salt: Uint8Array,
75
+ expectedKey: Uint8Array,
76
+ params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
77
+ ): Promise<boolean> {
78
+ const derivedKey = await deriveKeyFromPassword(password, salt, params);
79
+
80
+ // Constant-time comparison to prevent timing attacks
81
+ if (derivedKey.length !== expectedKey.length) {
82
+ return false;
83
+ }
84
+
85
+ let result = 0;
86
+ for (let i = 0; i < derivedKey.length; i++) {
87
+ result |= derivedKey[i] ^ expectedKey[i];
88
+ }
89
+
90
+ return result === 0;
91
+ }
92
+
93
+ // ═══════════════════════════════════════════════════════════════════════════
94
+ // PARAMETER ESTIMATION
95
+ // ═══════════════════════════════════════════════════════════════════════════
96
+
97
+ /**
98
+ * Estimate Argon2id parameters based on available memory and target time.
99
+ * Useful for adapting to low-memory environments.
100
+ *
101
+ * @param targetTimeMs - Target key derivation time in milliseconds
102
+ * @param availableMemoryMB - Available memory in megabytes
103
+ * @returns Estimated Argon2id parameters
104
+ */
105
+ export function estimateArgon2Params(
106
+ targetTimeMs: number = 1000,
107
+ availableMemoryMB: number = 64
108
+ ): Argon2idParams {
109
+ // Start with minimum parameters
110
+ const params = { ...MIN_ARGON2ID_PARAMS };
111
+
112
+ // Scale memory based on availability (cap at 64MB for browser compatibility)
113
+ const maxMemoryKB = Math.min(availableMemoryMB * 1024, 65536);
114
+ params.memoryCost = Math.max(MIN_ARGON2ID_PARAMS.memoryCost, maxMemoryKB);
115
+
116
+ // Use 4 parallelism for modern multi-core CPUs
117
+ params.parallelism = Math.min(4, navigator.hardwareConcurrency || 1);
118
+
119
+ // Estimate time cost (rough heuristic: 1 iteration ≈ 300ms at 64MB)
120
+ const estimatedIterations = Math.max(2, Math.floor(targetTimeMs / 300));
121
+ params.timeCost = Math.min(estimatedIterations, 10); // Cap at 10 iterations
122
+
123
+ return params;
124
+ }
125
+
126
+ /**
127
+ * Benchmark Argon2id on current device.
128
+ * Returns the time in milliseconds for one derivation.
129
+ *
130
+ * @param params - Parameters to benchmark
131
+ * @returns Time in milliseconds
132
+ */
133
+ export async function benchmarkArgon2(
134
+ params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS
135
+ ): Promise<number> {
136
+ const testPassword = 'benchmark-test-password';
137
+ const testSalt = generateArgon2Salt(params.saltLength);
138
+
139
+ const start = performance.now();
140
+ await deriveKeyFromPassword(testPassword, testSalt, params);
141
+ const end = performance.now();
142
+
143
+ return end - start;
144
+ }
145
+
146
+ // ═══════════════════════════════════════════════════════════════════════════
147
+ // AVAILABILITY CHECK
148
+ // ═══════════════════════════════════════════════════════════════════════════
149
+
150
+ let _argon2Available: boolean | null = null;
151
+
152
+ /**
153
+ * Check if Argon2id is available in current environment.
154
+ * Caches result after first check.
155
+ */
156
+ export async function isArgon2Available(): Promise<boolean> {
157
+ if (_argon2Available !== null) {
158
+ return _argon2Available;
159
+ }
160
+
161
+ try {
162
+ // Quick test with minimal parameters
163
+ await argon2id({
164
+ password: 'test',
165
+ salt: new Uint8Array(16),
166
+ parallelism: 1,
167
+ iterations: 1,
168
+ memorySize: 1024, // 1 MB
169
+ hashLength: 32,
170
+ outputType: 'binary',
171
+ });
172
+ _argon2Available = true;
173
+ } catch {
174
+ _argon2Available = false;
175
+ }
176
+
177
+ return _argon2Available;
178
+ }
179
+
180
+ // ═══════════════════════════════════════════════════════════════════════════
181
+ // EXPORTS
182
+ // ═══════════════════════════════════════════════════════════════════════════
183
+
184
+ export { DEFAULT_ARGON2ID_PARAMS, MIN_ARGON2ID_PARAMS };