@ursalock/crypto 0.2.1 → 0.3.1

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/dist/index.d.ts CHANGED
@@ -18,11 +18,15 @@ interface IEncryptedPayload {
18
18
  interface ICryptoProvider {
19
19
  /**
20
20
  * Encrypt data with AES-GCM
21
+ *
22
+ * The IV is always generated internally using a CSPRNG.
23
+ * Callers must NOT supply their own IV — reusing an IV with the same
24
+ * key under AES-GCM is catastrophic (NIST SP 800-38D §8.3).
25
+ *
21
26
  * @param plaintext Data to encrypt
22
27
  * @param key 256-bit encryption key
23
- * @param iv Optional IV (generated if not provided)
24
28
  */
25
- encrypt(plaintext: Uint8Array, key: Uint8Array, iv?: Uint8Array): Promise<IEncryptedPayload>;
29
+ encrypt(plaintext: Uint8Array, key: Uint8Array): Promise<IEncryptedPayload>;
26
30
  /**
27
31
  * Decrypt data with AES-GCM
28
32
  * @param encrypted Encrypted payload or combined bytes
@@ -47,14 +51,6 @@ interface IKeyDerivationProvider {
47
51
  salt: Uint8Array;
48
52
  }>;
49
53
  }
50
- /** Random number generator interface */
51
- interface IRandomProvider {
52
- /**
53
- * Generate cryptographically secure random bytes
54
- * @param length Number of bytes to generate
55
- */
56
- randomBytes(length: number): Uint8Array;
57
- }
58
54
 
59
55
  /**
60
56
  * Web Crypto API implementation (Concrete provider)
@@ -65,26 +61,34 @@ interface IRandomProvider {
65
61
  * Web Crypto API provider for AES-256-GCM
66
62
  */
67
63
  declare class WebCryptoProvider implements ICryptoProvider {
68
- encrypt(plaintext: Uint8Array, key: Uint8Array, iv?: Uint8Array): Promise<IEncryptedPayload>;
64
+ encrypt(plaintext: Uint8Array, key: Uint8Array): Promise<IEncryptedPayload>;
69
65
  decrypt(encrypted: Uint8Array | IEncryptedPayload, key: Uint8Array): Promise<Uint8Array>;
70
66
  }
71
67
 
72
68
  /**
73
69
  * Key derivation using Argon2id
74
70
  *
75
- * Parameters based on OWASP 2026 recommendations:
76
- * - Memory: 64 MiB
77
- * - Iterations: 3
71
+ * Current parameters based on OWASP 2026 high-security recommendations:
72
+ * - Memory: 128 MiB (OWASP "higher memory" profile)
73
+ * - Iterations: 4
78
74
  * - Parallelism: 4
75
+ *
76
+ * Legacy parameters (64 MiB / 3 iterations) are preserved for backward
77
+ * compatibility — existing vaults encrypted with the old defaults can
78
+ * still be decrypted by passing LEGACY_ARGON2_PARAMS explicitly.
79
+ *
80
+ * References:
81
+ * - OWASP Password Storage Cheat Sheet (2026 revision)
82
+ * - RFC 9106 §4 (Argon2 recommended parameters)
79
83
  */
80
84
  interface DeriveKeyOptions {
81
85
  /** Password or recovery key bytes */
82
86
  password: Uint8Array;
83
87
  /** Salt (32 bytes recommended, auto-generated if not provided) */
84
88
  salt?: Uint8Array;
85
- /** Memory cost in KiB (default: 65536 = 64 MiB) */
89
+ /** Memory cost in KiB (default: 131072 = 128 MiB) */
86
90
  memoryCost?: number;
87
- /** Time cost / iterations (default: 3) */
91
+ /** Time cost / iterations (default: 4) */
88
92
  timeCost?: number;
89
93
  /** Parallelism (default: 4) */
90
94
  parallelism?: number;
@@ -108,8 +112,44 @@ interface DerivedKey {
108
112
  * // Save salt alongside encrypted data
109
113
  * // Use key for AES-256-GCM encryption
110
114
  * ```
115
+ *
116
+ * @example Decrypt data encrypted with older parameters
117
+ * ```ts
118
+ * const { key } = await deriveKey({
119
+ * password: recoveryKeyBytes,
120
+ * salt: storedSalt,
121
+ * ...LEGACY_ARGON2_PARAMS,
122
+ * })
123
+ * ```
111
124
  */
112
125
  declare function deriveKey(options: DeriveKeyOptions): Promise<DerivedKey>;
126
+ /**
127
+ * Default parameters for key derivation
128
+ *
129
+ * OWASP 2026 high-security recommendation:
130
+ * - 128 MiB memory, 4 iterations, parallelism 4
131
+ * - Provides ≈ 2× the work factor of the previous defaults
132
+ */
133
+ declare const DEFAULT_ARGON2_PARAMS: {
134
+ readonly memoryCost: 131072;
135
+ readonly timeCost: 4;
136
+ readonly parallelism: 4;
137
+ readonly keyLength: 32;
138
+ readonly saltLength: 32;
139
+ };
140
+ /**
141
+ * Legacy Argon2id parameters (pre-2026)
142
+ *
143
+ * Use these when decrypting data that was encrypted with older defaults.
144
+ * New encryptions should always use DEFAULT_ARGON2_PARAMS.
145
+ */
146
+ declare const LEGACY_ARGON2_PARAMS: {
147
+ readonly memoryCost: 65536;
148
+ readonly timeCost: 3;
149
+ readonly parallelism: 4;
150
+ readonly keyLength: 32;
151
+ readonly saltLength: 32;
152
+ };
113
153
 
114
154
  /**
115
155
  * AES-256-GCM encryption/decryption using Web Crypto API
@@ -125,8 +165,7 @@ declare function deriveKey(options: DeriveKeyOptions): Promise<DerivedKey>;
125
165
  */
126
166
 
127
167
  /** Encrypted payload structure (backward compatibility) */
128
- interface EncryptedPayload extends IEncryptedPayload {
129
- }
168
+ type EncryptedPayload = IEncryptedPayload;
130
169
  /**
131
170
  * Set custom crypto provider (for testing or alternative implementations)
132
171
  * @param provider Custom crypto provider
@@ -230,6 +269,13 @@ declare function recoveryKeyToBytes(key: string): Uint8Array;
230
269
  * @returns Raw base32 string (no dashes)
231
270
  */
232
271
  declare function bytesToRecoveryKey(bytes: Uint8Array): string;
272
+ /**
273
+ * Format recovery key with dashes for readability
274
+ *
275
+ * @param raw - Raw base32 string
276
+ * @returns Formatted string with dashes every 4 characters
277
+ */
278
+ declare function formatRecoveryKey(raw: string): string;
233
279
 
234
280
  /**
235
281
  * Crypto utilities
@@ -243,6 +289,39 @@ declare function randomBytes(length: number): Uint8Array;
243
289
  */
244
290
  declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean;
245
291
 
292
+ /**
293
+ * HMAC-SHA256 using Web Crypto API
294
+ *
295
+ * Provides integrity verification for ciphertext in transit/storage.
296
+ * Uses HMAC-SHA256 as recommended by NIST SP 800-107 Rev. 1 §5.3
297
+ * and RFC 2104.
298
+ *
299
+ * Design rationale:
300
+ * - Separate HMAC key from encryption key (key separation principle)
301
+ * - Verify HMAC *before* attempting decryption (Encrypt-then-MAC)
302
+ * - Constant-time comparison via Web Crypto verify to prevent timing attacks
303
+ */
304
+ /**
305
+ * Compute HMAC-SHA256 over arbitrary data
306
+ *
307
+ * @param data - Data to authenticate
308
+ * @param key - HMAC key (any length; 32 bytes recommended)
309
+ * @returns Hex-encoded HMAC string (64 characters)
310
+ */
311
+ declare function computeHmac(data: Uint8Array, key: Uint8Array): Promise<string>;
312
+ /**
313
+ * Verify an HMAC-SHA256 tag
314
+ *
315
+ * Uses Web Crypto's verify() which performs constant-time comparison
316
+ * internally, preventing timing side-channel attacks.
317
+ *
318
+ * @param data - Data that was authenticated
319
+ * @param key - HMAC key (same key used for computeHmac)
320
+ * @param expectedHmac - Hex-encoded HMAC to verify against
321
+ * @returns True if HMAC is valid
322
+ */
323
+ declare function verifyHmac(data: Uint8Array, key: Uint8Array, expectedHmac: string): Promise<boolean>;
324
+
246
325
  /**
247
326
  * JWK-based encryption using @z-base/cryptosuite
248
327
  * For use with ZKCredentials PRF-derived keys
@@ -282,4 +361,4 @@ declare function encryptStringWithJwk(plaintext: string, cipherJwk: CipherJWK):
282
361
  */
283
362
  declare function decryptStringWithJwk(encrypted: Uint8Array | JwkEncryptedPayload, cipherJwk: CipherJWK): Promise<string>;
284
363
 
285
- export { type DeriveKeyOptions, type EncryptedPayload, type ICryptoProvider, type IEncryptedPayload, type IKeyDerivationProvider, type IRandomProvider, type JwkEncryptedPayload, type RecoveryKey, WebCryptoProvider, bytesToRecoveryKey, constantTimeEqual, decrypt, decryptString, decryptStringWithJwk, decryptWithJwk, deriveKey, encrypt, encryptString, encryptStringWithJwk, encryptWithJwk, generateRecoveryKey, getCryptoProvider, randomBytes, recoveryKeyToBytes, setCryptoProvider, validateRecoveryKey };
364
+ export { DEFAULT_ARGON2_PARAMS, type DeriveKeyOptions, type DerivedKey, type EncryptedPayload, type ICryptoProvider, type IEncryptedPayload, type IKeyDerivationProvider, type JwkEncryptedPayload, LEGACY_ARGON2_PARAMS, type RecoveryKey, WebCryptoProvider, bytesToRecoveryKey, computeHmac, constantTimeEqual, decrypt, decryptString, decryptStringWithJwk, decryptWithJwk, deriveKey, encrypt, encryptString, encryptStringWithJwk, encryptWithJwk, formatRecoveryKey, generateRecoveryKey, getCryptoProvider, randomBytes, recoveryKeyToBytes, setCryptoProvider, validateRecoveryKey, verifyHmac };
package/dist/index.js CHANGED
@@ -26,14 +26,14 @@ function concatBytes(...arrays) {
26
26
  // src/providers/web-crypto.ts
27
27
  var IV_LENGTH = 12;
28
28
  var WebCryptoProvider = class {
29
- async encrypt(plaintext, key, iv) {
29
+ async encrypt(plaintext, key) {
30
30
  if (key.length !== 32) {
31
31
  throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
32
32
  }
33
- const actualIv = iv ?? randomBytes(IV_LENGTH);
33
+ const actualIv = randomBytes(IV_LENGTH);
34
34
  const cryptoKey = await crypto.subtle.importKey(
35
35
  "raw",
36
- key.buffer,
36
+ key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength),
37
37
  { name: "AES-GCM" },
38
38
  false,
39
39
  ["encrypt"]
@@ -42,7 +42,7 @@ var WebCryptoProvider = class {
42
42
  await crypto.subtle.encrypt(
43
43
  { name: "AES-GCM", iv: actualIv },
44
44
  cryptoKey,
45
- plaintext.buffer
45
+ plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength)
46
46
  )
47
47
  );
48
48
  const combined = concatBytes(actualIv, ciphertext);
@@ -66,7 +66,7 @@ var WebCryptoProvider = class {
66
66
  }
67
67
  const cryptoKey = await crypto.subtle.importKey(
68
68
  "raw",
69
- key.buffer,
69
+ key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength),
70
70
  { name: "AES-GCM" },
71
71
  false,
72
72
  ["decrypt"]
@@ -75,7 +75,7 @@ var WebCryptoProvider = class {
75
75
  const plaintext = await crypto.subtle.decrypt(
76
76
  { name: "AES-GCM", iv },
77
77
  cryptoKey,
78
- ciphertext.buffer
78
+ ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength)
79
79
  );
80
80
  return new Uint8Array(plaintext);
81
81
  } catch (error) {
@@ -90,9 +90,9 @@ async function deriveKey(options) {
90
90
  const {
91
91
  password,
92
92
  salt = randomBytes(32),
93
- memoryCost = 65536,
94
- // 64 MiB
95
- timeCost = 3,
93
+ memoryCost = 131072,
94
+ // 128 MiB — OWASP 2026 high-security recommendation
95
+ timeCost = 4,
96
96
  parallelism = 4,
97
97
  keyLength = 32
98
98
  } = options;
@@ -110,6 +110,22 @@ async function deriveKey(options) {
110
110
  salt
111
111
  };
112
112
  }
113
+ var DEFAULT_ARGON2_PARAMS = {
114
+ memoryCost: 131072,
115
+ // 128 MiB
116
+ timeCost: 4,
117
+ parallelism: 4,
118
+ keyLength: 32,
119
+ saltLength: 32
120
+ };
121
+ var LEGACY_ARGON2_PARAMS = {
122
+ memoryCost: 65536,
123
+ // 64 MiB
124
+ timeCost: 3,
125
+ parallelism: 4,
126
+ keyLength: 32,
127
+ saltLength: 32
128
+ };
113
129
 
114
130
  // src/aes.ts
115
131
  var defaultProvider = new WebCryptoProvider();
@@ -145,10 +161,13 @@ function generateRecoveryKey() {
145
161
  function validateRecoveryKey(key) {
146
162
  const clean = key.replace(/[-\s]/g, "").toUpperCase();
147
163
  if (clean.length !== 52) return false;
148
- for (const char of clean) {
149
- if (!BASE32_ALPHABET.includes(char)) return false;
164
+ let valid = true;
165
+ for (let i = 0; i < clean.length; i++) {
166
+ if (!BASE32_ALPHABET.includes(clean[i])) {
167
+ valid = false;
168
+ }
150
169
  }
151
- return true;
170
+ return valid;
152
171
  }
153
172
  function recoveryKeyToBytes(key) {
154
173
  const clean = key.replace(/[-\s]/g, "").toUpperCase();
@@ -206,6 +225,52 @@ function base32Decode(str) {
206
225
  return new Uint8Array(output);
207
226
  }
208
227
 
228
+ // src/hmac.ts
229
+ async function computeHmac(data, key) {
230
+ const cryptoKey = await crypto.subtle.importKey(
231
+ "raw",
232
+ key,
233
+ { name: "HMAC", hash: "SHA-256" },
234
+ false,
235
+ ["sign"]
236
+ );
237
+ const signature = new Uint8Array(
238
+ await crypto.subtle.sign("HMAC", cryptoKey, data)
239
+ );
240
+ return bytesToHex(signature);
241
+ }
242
+ async function verifyHmac(data, key, expectedHmac) {
243
+ const cryptoKey = await crypto.subtle.importKey(
244
+ "raw",
245
+ key,
246
+ { name: "HMAC", hash: "SHA-256" },
247
+ false,
248
+ ["verify"]
249
+ );
250
+ const expectedBytes = hexToBytes(expectedHmac);
251
+ return crypto.subtle.verify(
252
+ "HMAC",
253
+ cryptoKey,
254
+ expectedBytes,
255
+ data
256
+ );
257
+ }
258
+ function bytesToHex(bytes) {
259
+ let hex = "";
260
+ for (let i = 0; i < bytes.length; i++) {
261
+ hex += bytes[i].toString(16).padStart(2, "0");
262
+ }
263
+ return hex;
264
+ }
265
+ function hexToBytes(hex) {
266
+ const len = hex.length >>> 1;
267
+ const bytes = new Uint8Array(len);
268
+ for (let i = 0; i < len; i++) {
269
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
270
+ }
271
+ return bytes;
272
+ }
273
+
209
274
  // src/jwk.ts
210
275
  import { CipherCluster } from "@z-base/cryptosuite";
211
276
  async function encryptWithJwk(plaintext, cipherJwk) {
@@ -250,8 +315,11 @@ async function decryptStringWithJwk(encrypted, cipherJwk) {
250
315
  return new TextDecoder().decode(plaintext);
251
316
  }
252
317
  export {
318
+ DEFAULT_ARGON2_PARAMS,
319
+ LEGACY_ARGON2_PARAMS,
253
320
  WebCryptoProvider,
254
321
  bytesToRecoveryKey,
322
+ computeHmac,
255
323
  constantTimeEqual,
256
324
  decrypt,
257
325
  decryptString,
@@ -262,10 +330,12 @@ export {
262
330
  encryptString,
263
331
  encryptStringWithJwk,
264
332
  encryptWithJwk,
333
+ formatRecoveryKey,
265
334
  generateRecoveryKey,
266
335
  getCryptoProvider,
267
336
  randomBytes,
268
337
  recoveryKeyToBytes,
269
338
  setCryptoProvider,
270
- validateRecoveryKey
339
+ validateRecoveryKey,
340
+ verifyHmac
271
341
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ursalock/crypto",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "E2EE crypto primitives for ursalock",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",