@crossdyne/security 0.5.0-beta.2 → 2.0.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.js CHANGED
@@ -1,12 +1,14 @@
1
1
  // src/configurations/security-constants.ts
2
2
  var SecurityConstants = {
3
+ /** Standard nonce size for AES-GCM (96 bits). Fixed by NIST SP 800-38D. */
3
4
  AesGcmNonceSize: 12,
4
- AesGcmTagSize: 16,
5
+ /** Minimum allowed authentication tag size (96 bits). */
5
6
  AesGcmTagSizeMin: 12,
7
+ /** Maximum allowed authentication tag size (128 bits). */
6
8
  AesGcmTagSizeMax: 16,
9
+ /** Key size for AES-256 (256 bits). */
7
10
  KeySizeBytes: 32,
8
- // 256 bits
9
- Pbkdf2IterationsDefault: 6e5,
11
+ /** Absolute minimum PBKDF2 iterations for any profile version. */
10
12
  Pbkdf2IterationsMinimum: 1e5
11
13
  };
12
14
  var HashSizes = {
@@ -108,54 +110,36 @@ var SecurityUtils = class {
108
110
  };
109
111
 
110
112
  // src/crypto/aes-gcm-options.ts
111
- var AesGcmOptions = class _AesGcmOptions {
112
- constructor() {
113
- this._nonceSize = SecurityConstants.AesGcmNonceSize;
114
- this._tagSize = SecurityConstants.AesGcmTagSize;
115
- }
116
- /** Nonce size in bytes (must be 12). */
117
- get nonceSize() {
118
- return this._nonceSize;
119
- }
120
- set nonceSize(v) {
121
- if (v !== SecurityConstants.AesGcmNonceSize) throw new RangeError(`AES-GCM requires ${SecurityConstants.AesGcmNonceSize}-byte nonce`);
122
- this._nonceSize = v;
123
- }
124
- /** Tag size in bytes (12–16, default 16). */
125
- get tagSize() {
126
- return this._tagSize;
127
- }
128
- set tagSize(v) {
129
- if (v < SecurityConstants.AesGcmTagSizeMin || v > SecurityConstants.AesGcmTagSizeMax) throw new RangeError(`Tag size must be between ${SecurityConstants.AesGcmTagSizeMin} and ${SecurityConstants.AesGcmTagSizeMax}`);
130
- this._tagSize = v;
113
+ var _AesGcmOptions = class _AesGcmOptions {
114
+ constructor(nonceSize, tagSize) {
115
+ if (nonceSize !== SecurityConstants.AesGcmNonceSize) {
116
+ throw new RangeError(
117
+ `AES-GCM requires exactly ${SecurityConstants.AesGcmNonceSize}-byte nonce per NIST SP 800-38D.`
118
+ );
119
+ }
120
+ if (tagSize < SecurityConstants.AesGcmTagSizeMin || tagSize > SecurityConstants.AesGcmTagSizeMax) {
121
+ throw new RangeError(
122
+ `Tag size must be between ${SecurityConstants.AesGcmTagSizeMin} and ${SecurityConstants.AesGcmTagSizeMax} bytes.`
123
+ );
124
+ }
125
+ this.nonceSize = nonceSize;
126
+ this.tagSize = tagSize;
127
+ Object.freeze(this);
131
128
  }
132
129
  /** Validates that {@link tagSize} is in the allowed range. */
133
130
  validate() {
134
- if (this.tagSize < SecurityConstants.AesGcmTagSizeMin || this.tagSize > SecurityConstants.AesGcmTagSizeMax) throw new Error(`Invalid Tag Size: ${this.tagSize}`);
135
- }
136
- /** Default preset: nonce=12, tag=16, no AAD. */
137
- static get default() {
138
- return new _AesGcmOptions();
139
- }
140
- /** Fluent setter for {@link tagSize}. */
141
- withTagSize(s) {
142
- this.tagSize = s;
143
- return this;
144
- }
145
- /**
146
- * Fluent setter for {@link associatedData}.
147
- * Accepts a byte array or a UTF-8 string (encoded internally).
148
- */
149
- withAssociatedData(aad) {
150
- this.associatedData = typeof aad === "string" ? new TextEncoder().encode(aad) : aad;
151
- return this;
152
- }
153
- /** Validates and returns this instance. */
154
- build() {
155
- this.validate();
156
- return this;
131
+ if (this.tagSize < SecurityConstants.AesGcmTagSizeMin || this.tagSize > SecurityConstants.AesGcmTagSizeMax)
132
+ throw new Error(`Invalid Tag Size: ${this.tagSize}`);
157
133
  }
158
134
  };
135
+ /**
136
+ * V1 preset: nonce=12, tag=16, no AAD.
137
+ * These exact values are frozen for all V1-encrypted payloads.
138
+ */
139
+ _AesGcmOptions.V1 = Object.freeze(
140
+ new _AesGcmOptions(12, 16)
141
+ );
142
+ var AesGcmOptions = _AesGcmOptions;
159
143
 
160
144
  // src/crypto/crypto-profile.ts
161
145
  var CryptoProfile = class {
@@ -165,6 +149,7 @@ var CryptoProfile = class {
165
149
  */
166
150
  constructor(params) {
167
151
  this.version = params.version;
152
+ this.algorithmName = params.algorithmName;
168
153
  this.kdfOptions = params.kdfOptions;
169
154
  this.aesGcmOptions = params.aesGcmOptions;
170
155
  }
@@ -177,28 +162,21 @@ var CryptoVersion = /* @__PURE__ */ ((CryptoVersion2) => {
177
162
  })(CryptoVersion || {});
178
163
 
179
164
  // src/crypto/kdf-options.ts
180
- var KdfOptions = class _KdfOptions {
181
- constructor() {
182
- this._pbkdf2Iterations = SecurityConstants.Pbkdf2IterationsDefault;
183
- this._hashAlgorithm = "SHA-256";
184
- }
185
- /** PBKDF2 iteration count (minimum 100_000). */
186
- get pbkdf2Iterations() {
187
- return this._pbkdf2Iterations;
188
- }
189
- set pbkdf2Iterations(v) {
190
- if (v < SecurityConstants.Pbkdf2IterationsMinimum)
191
- throw new RangeError(`PBKDF2 iterations must be \u2265 ${SecurityConstants.Pbkdf2IterationsMinimum}`);
192
- this._pbkdf2Iterations = v;
193
- }
194
- /** Hash algorithm used by PBKDF2. Must be one of {@link SupportedHashAlgorithms}. */
195
- get hashAlgorithm() {
196
- return this._hashAlgorithm;
197
- }
198
- set hashAlgorithm(v) {
199
- if (!SupportedHashAlgorithms.includes(v))
200
- throw new RangeError(`Unsupported hash: ${v}`);
201
- this._hashAlgorithm = v;
165
+ var _KdfOptions = class _KdfOptions {
166
+ constructor(pbkdf2Iterations, hashAlgorithm) {
167
+ if (pbkdf2Iterations < SecurityConstants.Pbkdf2IterationsMinimum) {
168
+ throw new RangeError(
169
+ `PBKDF2 iterations must be at least ${SecurityConstants.Pbkdf2IterationsMinimum}.`
170
+ );
171
+ }
172
+ if (!SupportedHashAlgorithms.includes(hashAlgorithm)) {
173
+ throw new RangeError(
174
+ `Unsupported hash algorithm: ${hashAlgorithm}. Supported: ${SupportedHashAlgorithms.join(", ")}.`
175
+ );
176
+ }
177
+ this.pbkdf2Iterations = pbkdf2Iterations;
178
+ this.hashAlgorithm = hashAlgorithm;
179
+ Object.freeze(this);
202
180
  }
203
181
  /** Validates iterations and hash algorithm. */
204
182
  validate() {
@@ -207,46 +185,33 @@ var KdfOptions = class _KdfOptions {
207
185
  if (!SupportedHashAlgorithms.includes(this.hashAlgorithm))
208
186
  throw new Error(`Invalid hash algorithm: ${this.hashAlgorithm}`);
209
187
  }
210
- /** Default preset: SHA-256, 600_000 iterations. */
211
- static get default() {
212
- return new _KdfOptions();
213
- }
214
- /** Fluent setter for {@link pbkdf2Iterations}. */
215
- withPbkdf2Iterations(i) {
216
- this.pbkdf2Iterations = i;
217
- return this;
218
- }
219
- /** Fluent setter for {@link hashAlgorithm}. */
220
- withHashAlgorithm(h) {
221
- this.hashAlgorithm = h;
222
- return this;
223
- }
224
- /** Validates and returns this instance. */
225
- build() {
226
- this.validate();
227
- return this;
228
- }
229
188
  };
189
+ /**
190
+ * V1 preset: SHA-256, 600_000 iterations.
191
+ * These exact values are frozen for all V1-derived keys.
192
+ */
193
+ _KdfOptions.V1 = Object.freeze(
194
+ new _KdfOptions(6e5, "SHA-256")
195
+ );
196
+ var KdfOptions = _KdfOptions;
230
197
 
231
198
  // src/crypto/crypto-profile-registry.ts
232
199
  var CryptoProfileRegistry = class {
233
200
  static getProfile(version) {
234
201
  switch (version) {
235
202
  case 1 /* V1 */:
236
- return new CryptoProfile({
237
- version: 1 /* V1 */,
238
- kdfOptions: KdfOptions.default,
239
- aesGcmOptions: AesGcmOptions.default
240
- });
203
+ return this.V1_PROFILE;
241
204
  default:
242
205
  throw new Error(`Unsupported crypto version: ${version}`);
243
206
  }
244
207
  }
245
- /** Latest supported profile (currently V1). */
246
- static get latest() {
247
- return this.getProfile(1 /* V1 */);
248
- }
249
208
  };
209
+ CryptoProfileRegistry.V1_PROFILE = new CryptoProfile({
210
+ version: 1 /* V1 */,
211
+ algorithmName: "AES-GCM",
212
+ kdfOptions: KdfOptions.V1,
213
+ aesGcmOptions: AesGcmOptions.V1
214
+ });
250
215
 
251
216
  // src/crypto/crypto.service.ts
252
217
  var CryptoService = class {
@@ -261,7 +226,7 @@ var CryptoService = class {
261
226
  /**
262
227
  * Encrypts a serializable object to a Base64 string.
263
228
  * @param dataModel - Object or Uint8Array to encrypt.
264
- * @param key - AES-256 key (32 bytes).
229
+ * @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
265
230
  * @returns Base64-encoded ciphertext with prepended nonce.
266
231
  */
267
232
  async encryptData(dataModel, key, version = 1 /* V1 */) {
@@ -277,25 +242,13 @@ var CryptoService = class {
277
242
  }
278
243
  const plainBytes = encoder.encode(jsonString);
279
244
  const nonce = crypto.getRandomValues(new Uint8Array(opts.nonceSize));
280
- const cryptoKey = await crypto.subtle.importKey(
281
- "raw",
282
- key,
283
- "AES-GCM",
284
- false,
285
- ["encrypt"]
286
- );
287
- let associatedData = new Uint8Array(0);
288
- if (opts.associatedData != null) {
289
- associatedData = opts.associatedData;
290
- }
291
245
  const encryptedContent = await crypto.subtle.encrypt(
292
246
  {
293
247
  name: "AES-GCM",
294
248
  iv: nonce,
295
- tagLength: opts.tagSize * 8,
296
- additionalData: associatedData
249
+ tagLength: opts.tagSize * 8
297
250
  },
298
- cryptoKey,
251
+ key,
299
252
  plainBytes
300
253
  );
301
254
  const result = new Uint8Array(1 + opts.nonceSize + encryptedContent.byteLength);
@@ -307,7 +260,7 @@ var CryptoService = class {
307
260
  /**
308
261
  * Decrypts a Base64-encoded ciphertext back to the original object.
309
262
  * @param encryptedBase64 - The encrypted data.
310
- * @param key - AES-256 key (32 bytes).
263
+ * @param key - A non-extractable CryptoKey obtained via {@link CryptoService.importKey}.
311
264
  * @returns Deserialized object, or null if input is empty.
312
265
  * @throws If authentication tag mismatch or corrupted data.
313
266
  */
@@ -324,25 +277,14 @@ var CryptoService = class {
324
277
  throw new Error(`Invalid format: minimum expected ${opts.nonceSize + opts.tagSize} byte.`);
325
278
  const nonce = payload.slice(0, opts.nonceSize);
326
279
  const ciphertextWithTag = payload.slice(opts.nonceSize);
327
- const cryptoKey = await crypto.subtle.importKey(
328
- "raw",
329
- key,
330
- "AES-GCM",
331
- false,
332
- ["decrypt"]
333
- );
334
280
  try {
335
- let associatedData = new Uint8Array(0);
336
- if (opts.associatedData != null)
337
- associatedData = opts.associatedData;
338
281
  const decryptedBuffer = await crypto.subtle.decrypt(
339
282
  {
340
283
  name: "AES-GCM",
341
284
  iv: nonce,
342
- tagLength: opts.tagSize * 8,
343
- additionalData: associatedData
285
+ tagLength: opts.tagSize * 8
344
286
  },
345
- cryptoKey,
287
+ key,
346
288
  ciphertextWithTag
347
289
  );
348
290
  const decoder = new TextDecoder();
@@ -357,6 +299,28 @@ var CryptoService = class {
357
299
  throw new Error("Decryption failed: authentication tag mismatch or corrupted data.");
358
300
  }
359
301
  }
302
+ /**
303
+ * Imports raw key bytes into a non-extractable CryptoKey based on the crypto profile version.
304
+ *
305
+ * The resulting key cannot be exported back to raw bytes (extractable: false),
306
+ * ensuring that sensitive key material does not persist in JavaScript-accessible memory.
307
+ * Callers should securely wipe the original raw key buffer immediately after import.
308
+ *
309
+ * @param rawKey - Raw key bytes (e.g. a Data Encryption Key).
310
+ * @param version - Crypto profile version that determines the algorithm and parameters.
311
+ * @param usages - Allowed key operations (e.g. ['encrypt', 'decrypt']).
312
+ * @returns A non-extractable CryptoKey bound to the algorithm defined by the profile.
313
+ */
314
+ async importKey(rawKey, version, usages) {
315
+ const profile = CryptoProfileRegistry.getProfile(version);
316
+ return await crypto.subtle.importKey(
317
+ "raw",
318
+ rawKey,
319
+ profile.algorithmName,
320
+ false,
321
+ usages
322
+ );
323
+ }
360
324
  };
361
325
 
362
326
  // src/crypto/key-derivation.service.ts