@ursalock/crypto 0.2.1 → 0.3.0
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 +98 -19
- package/dist/index.js +83 -13
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
*
|
|
76
|
-
* - Memory:
|
|
77
|
-
* - Iterations:
|
|
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:
|
|
89
|
+
/** Memory cost in KiB (default: 131072 = 128 MiB) */
|
|
86
90
|
memoryCost?: number;
|
|
87
|
-
/** Time cost / iterations (default:
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
94
|
-
//
|
|
95
|
-
timeCost =
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
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
|
};
|