@memberjunction/encryption 0.0.1 → 2.130.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.
Files changed (46) hide show
  1. package/README.md +391 -28
  2. package/dist/EncryptionEngine.d.ts +351 -0
  3. package/dist/EncryptionEngine.d.ts.map +1 -0
  4. package/dist/EncryptionEngine.js +683 -0
  5. package/dist/EncryptionEngine.js.map +1 -0
  6. package/dist/EncryptionKeySourceBase.d.ts +203 -0
  7. package/dist/EncryptionKeySourceBase.d.ts.map +1 -0
  8. package/dist/EncryptionKeySourceBase.js +133 -0
  9. package/dist/EncryptionKeySourceBase.js.map +1 -0
  10. package/dist/actions/EnableFieldEncryptionAction.d.ts +87 -0
  11. package/dist/actions/EnableFieldEncryptionAction.d.ts.map +1 -0
  12. package/dist/actions/EnableFieldEncryptionAction.js +308 -0
  13. package/dist/actions/EnableFieldEncryptionAction.js.map +1 -0
  14. package/dist/actions/RotateEncryptionKeyAction.d.ts +79 -0
  15. package/dist/actions/RotateEncryptionKeyAction.d.ts.map +1 -0
  16. package/dist/actions/RotateEncryptionKeyAction.js +343 -0
  17. package/dist/actions/RotateEncryptionKeyAction.js.map +1 -0
  18. package/dist/actions/index.d.ts +12 -0
  19. package/dist/actions/index.d.ts.map +1 -0
  20. package/dist/actions/index.js +17 -0
  21. package/dist/actions/index.js.map +1 -0
  22. package/dist/index.d.ts +66 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +81 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interfaces.d.ts +216 -0
  27. package/dist/interfaces.d.ts.map +1 -0
  28. package/dist/interfaces.js +15 -0
  29. package/dist/interfaces.js.map +1 -0
  30. package/dist/providers/AWSKMSKeySource.d.ts +110 -0
  31. package/dist/providers/AWSKMSKeySource.d.ts.map +1 -0
  32. package/dist/providers/AWSKMSKeySource.js +245 -0
  33. package/dist/providers/AWSKMSKeySource.js.map +1 -0
  34. package/dist/providers/AzureKeyVaultKeySource.d.ts +109 -0
  35. package/dist/providers/AzureKeyVaultKeySource.d.ts.map +1 -0
  36. package/dist/providers/AzureKeyVaultKeySource.js +268 -0
  37. package/dist/providers/AzureKeyVaultKeySource.js.map +1 -0
  38. package/dist/providers/ConfigFileKeySource.d.ts +173 -0
  39. package/dist/providers/ConfigFileKeySource.d.ts.map +1 -0
  40. package/dist/providers/ConfigFileKeySource.js +310 -0
  41. package/dist/providers/ConfigFileKeySource.js.map +1 -0
  42. package/dist/providers/EnvVarKeySource.d.ts +152 -0
  43. package/dist/providers/EnvVarKeySource.d.ts.map +1 -0
  44. package/dist/providers/EnvVarKeySource.js +251 -0
  45. package/dist/providers/EnvVarKeySource.js.map +1 -0
  46. package/package.json +65 -6
@@ -0,0 +1,683 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Core encryption engine for MemberJunction field-level encryption.
4
+ *
5
+ * The EncryptionEngine is the central class for all encryption and decryption
6
+ * operations. It provides:
7
+ *
8
+ * - AES-256-GCM/CBC encryption with authenticated encryption support
9
+ * - Pluggable key sources via the ClassFactory pattern
10
+ * - Multi-level caching for performance (inherited from EncryptionEngineBase)
11
+ * - Self-describing encrypted value format
12
+ * - Key rotation support with explicit lookup overrides
13
+ *
14
+ * ## Usage
15
+ *
16
+ * ```typescript
17
+ * import { EncryptionEngine } from '@memberjunction/encryption';
18
+ *
19
+ * // First, configure the engine to load metadata
20
+ * await EncryptionEngine.Instance.Config(false, contextUser);
21
+ *
22
+ * // Encrypt a value
23
+ * const encrypted = await EncryptionEngine.Instance.Encrypt(
24
+ * 'sensitive-data',
25
+ * encryptionKeyId,
26
+ * contextUser
27
+ * );
28
+ *
29
+ * // Decrypt a value
30
+ * const decrypted = await EncryptionEngine.Instance.Decrypt(encrypted, contextUser);
31
+ *
32
+ * // Check if a value is encrypted
33
+ * if (EncryptionEngine.Instance.IsEncrypted(someValue)) {
34
+ * // Handle encrypted value
35
+ * }
36
+ * ```
37
+ *
38
+ * ## Security Design
39
+ *
40
+ * - Keys are never stored in memory longer than needed
41
+ * - Authenticated encryption (GCM) prevents tampering
42
+ * - Random IVs for each encryption operation
43
+ * - Self-describing format enables proper key lookup
44
+ * - Secure defaults (fail-safe on errors)
45
+ *
46
+ * @module @memberjunction/encryption
47
+ */
48
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
49
+ if (k2 === undefined) k2 = k;
50
+ var desc = Object.getOwnPropertyDescriptor(m, k);
51
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
52
+ desc = { enumerable: true, get: function() { return m[k]; } };
53
+ }
54
+ Object.defineProperty(o, k2, desc);
55
+ }) : (function(o, m, k, k2) {
56
+ if (k2 === undefined) k2 = k;
57
+ o[k2] = m[k];
58
+ }));
59
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
60
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
61
+ }) : function(o, v) {
62
+ o["default"] = v;
63
+ });
64
+ var __importStar = (this && this.__importStar) || function (mod) {
65
+ if (mod && mod.__esModule) return mod;
66
+ var result = {};
67
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
68
+ __setModuleDefault(result, mod);
69
+ return result;
70
+ };
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.EncryptionEngine = void 0;
73
+ const crypto = __importStar(require("crypto"));
74
+ const global_1 = require("@memberjunction/global");
75
+ const core_1 = require("@memberjunction/core");
76
+ const core_entities_1 = require("@memberjunction/core-entities");
77
+ const EncryptionKeySourceBase_1 = require("./EncryptionKeySourceBase");
78
+ /**
79
+ * Default cache time-to-live in milliseconds (5 minutes).
80
+ * Balances security (key changes take effect) with performance.
81
+ */
82
+ const DEFAULT_KEY_MATERIAL_CACHE_TTL_MS = 5 * 60 * 1000;
83
+ /**
84
+ * Core encryption engine for field-level encryption operations.
85
+ *
86
+ * This class extends EncryptionEngineBase to inherit metadata caching for
87
+ * encryption keys, algorithms, and key sources. It adds the actual
88
+ * encryption/decryption operations using Node.js crypto.
89
+ *
90
+ * Use `EncryptionEngine.Instance` to access the singleton.
91
+ *
92
+ * ## Thread Safety
93
+ *
94
+ * The engine is designed to be safe for concurrent use in async contexts.
95
+ * Cache operations are atomic Map operations and crypto operations
96
+ * use per-call state.
97
+ *
98
+ * ## Error Handling
99
+ *
100
+ * The engine throws descriptive errors for:
101
+ * - Missing keys or configurations
102
+ * - Invalid encrypted value format
103
+ * - Decryption failures (including auth tag mismatch)
104
+ * - Key length mismatches
105
+ *
106
+ * Callers should catch and handle errors appropriately.
107
+ */
108
+ class EncryptionEngine extends core_entities_1.EncryptionEngineBase {
109
+ /**
110
+ * Cache for decrypted key material.
111
+ * Maps 'keyId:version' to Buffer.
112
+ * This is separate from the base class caches since key material
113
+ * is sensitive and needs different handling.
114
+ *
115
+ * @private
116
+ */
117
+ _keyMaterialCache = new Map();
118
+ /**
119
+ * Cache for initialized key source instances.
120
+ * Maps driver class name to provider instance.
121
+ *
122
+ * @private
123
+ */
124
+ _keySourceCache = new Map();
125
+ /**
126
+ * Cache TTL for key material in milliseconds.
127
+ * Can be configured for testing or specific deployment needs.
128
+ *
129
+ * @private
130
+ */
131
+ _keyMaterialCacheTtlMs = DEFAULT_KEY_MATERIAL_CACHE_TTL_MS;
132
+ /**
133
+ * Gets the singleton instance of the encryption engine.
134
+ *
135
+ * The instance is created on first access and reused thereafter.
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * const engine = EncryptionEngine.Instance;
140
+ * await engine.Config(false, contextUser);
141
+ * const encrypted = await engine.Encrypt(data, keyId, contextUser);
142
+ * ```
143
+ */
144
+ static get Instance() {
145
+ return super.getInstance();
146
+ }
147
+ /**
148
+ * Configures the engine by loading encryption metadata from the database.
149
+ *
150
+ * This overrides the base Config to ensure proper initialization.
151
+ * Must be called before performing encryption/decryption operations.
152
+ *
153
+ * @param forceRefresh - If true, reloads data even if already loaded
154
+ * @param contextUser - User context for database access (required server-side)
155
+ * @param provider - Optional metadata provider override
156
+ */
157
+ async Config(forceRefresh, contextUser, provider) {
158
+ await super.Config(forceRefresh, contextUser, provider);
159
+ }
160
+ /**
161
+ * Encrypts a value using the specified encryption key.
162
+ *
163
+ * The method:
164
+ * 1. Gets the key configuration from cached metadata
165
+ * 2. Retrieves key material from the configured source (cached)
166
+ * 3. Generates a random IV
167
+ * 4. Encrypts the data using the configured algorithm
168
+ * 5. Returns a self-describing encrypted string
169
+ *
170
+ * ## Encrypted Format
171
+ *
172
+ * The result format is:
173
+ * `$ENC$<keyId>$<algorithm>$<iv>$<ciphertext>[$<authTag>]`
174
+ *
175
+ * This format contains all information needed for decryption.
176
+ *
177
+ * @param plaintext - The value to encrypt (string or Buffer)
178
+ * @param encryptionKeyId - UUID of the encryption key to use
179
+ * @param contextUser - User context for database access
180
+ * @returns The encrypted value as a string
181
+ *
182
+ * @throws Error if the key cannot be found or is invalid
183
+ * @throws Error if key material retrieval fails
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const encrypted = await engine.Encrypt(
188
+ * 'secret-api-key',
189
+ * '550e8400-e29b-41d4-a716-446655440000',
190
+ * currentUser
191
+ * );
192
+ * // Returns: $ENC$550e8400-....$AES-256-GCM$<iv>$<ciphertext>$<authTag>
193
+ * ```
194
+ */
195
+ async Encrypt(plaintext, encryptionKeyId, contextUser) {
196
+ // Handle null/undefined - return as-is (cannot encrypt nothing)
197
+ if (plaintext === null || plaintext === undefined) {
198
+ return plaintext;
199
+ }
200
+ // Validate the key ID format
201
+ if (!encryptionKeyId || !this.isValidUUID(encryptionKeyId)) {
202
+ throw new Error(`Invalid encryption key ID: "${encryptionKeyId}". ` +
203
+ 'Must be a valid UUID.');
204
+ }
205
+ // Ensure engine is configured
206
+ await this.ensureConfigured(contextUser);
207
+ // Get key configuration from cached metadata
208
+ const keyConfig = this.buildKeyConfiguration(encryptionKeyId);
209
+ // Get the key material
210
+ const keyMaterial = await this.getKeyMaterial(keyConfig);
211
+ // Perform encryption
212
+ return this.performEncryption(plaintext, keyConfig, keyMaterial);
213
+ }
214
+ /**
215
+ * Decrypts an encrypted value.
216
+ *
217
+ * If the value is not encrypted (doesn't start with marker), returns it unchanged.
218
+ *
219
+ * The method:
220
+ * 1. Parses the encrypted value to extract key ID and parameters
221
+ * 2. Gets the key configuration from cached metadata
222
+ * 3. Retrieves key material from the configured source (cached)
223
+ * 4. Decrypts using the algorithm and IV from the encrypted value
224
+ * 5. Verifies the auth tag for AEAD algorithms
225
+ *
226
+ * @param value - The value to decrypt (may or may not be encrypted)
227
+ * @param contextUser - User context for database access
228
+ * @returns The decrypted plaintext, or original value if not encrypted
229
+ *
230
+ * @throws Error if decryption fails (invalid key, corrupted data, auth tag mismatch)
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * // Decrypt an encrypted value
235
+ * const plaintext = await engine.Decrypt(encryptedValue, contextUser);
236
+ *
237
+ * // Non-encrypted values pass through unchanged
238
+ * const same = await engine.Decrypt('plain-text', contextUser);
239
+ * // Returns: 'plain-text'
240
+ * ```
241
+ */
242
+ async Decrypt(value, contextUser) {
243
+ // If not encrypted, return as-is
244
+ if (!this.IsEncrypted(value)) {
245
+ return value;
246
+ }
247
+ // Parse the encrypted value
248
+ const parsed = this.ParseEncryptedValue(value);
249
+ // Ensure engine is configured
250
+ await this.ensureConfigured(contextUser);
251
+ // Get key configuration from cached metadata
252
+ const keyConfig = this.buildKeyConfiguration(parsed.keyId);
253
+ // Get key material
254
+ const keyMaterial = await this.getKeyMaterial(keyConfig);
255
+ // Perform decryption
256
+ return this.performDecryption(parsed, keyConfig, keyMaterial);
257
+ }
258
+ /**
259
+ * Checks if a value is encrypted.
260
+ *
261
+ * Encrypted values start with the marker prefix (default: '$ENC$').
262
+ * This also checks for the encrypted sentinel value.
263
+ * This is a fast, synchronous check that doesn't require database access.
264
+ *
265
+ * @param value - The value to check
266
+ * @param encryptionMarker - Optional custom marker to check for (defaults to '$ENC$')
267
+ * @returns `true` if the value appears to be encrypted or is the sentinel value
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * if (engine.IsEncrypted(fieldValue)) {
272
+ * const decrypted = await engine.Decrypt(fieldValue, user);
273
+ * }
274
+ *
275
+ * // With custom marker from key
276
+ * const key = engine.GetKeyByID(keyId);
277
+ * if (engine.IsEncrypted(fieldValue, key?.Marker)) {
278
+ * const decrypted = await engine.Decrypt(fieldValue, user);
279
+ * }
280
+ * ```
281
+ */
282
+ IsEncrypted(value, encryptionMarker) {
283
+ return (0, global_1.IsValueEncrypted)(value, encryptionMarker);
284
+ }
285
+ /**
286
+ * Parses an encrypted value string into its component parts.
287
+ *
288
+ * Use this when you need to inspect the encrypted value without decrypting.
289
+ *
290
+ * @param value - The encrypted value string
291
+ * @returns Parsed components (marker, keyId, algorithm, iv, ciphertext, authTag)
292
+ *
293
+ * @throws Error if the format is invalid
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const parts = engine.ParseEncryptedValue(encryptedValue);
298
+ * console.log(`Encrypted with key: ${parts.keyId}`);
299
+ * console.log(`Algorithm: ${parts.algorithm}`);
300
+ * ```
301
+ */
302
+ ParseEncryptedValue(value) {
303
+ if (!value || typeof value !== 'string') {
304
+ throw new Error('Cannot parse encrypted value: input is null or not a string');
305
+ }
306
+ // Split on $ and filter empty parts
307
+ // Format: $ENC$keyId$algorithm$iv$ciphertext[$authTag]
308
+ const parts = value.split('$').filter(p => p !== '');
309
+ if (parts.length < 5) {
310
+ throw new Error(`Invalid encrypted value format: expected at least 5 parts ` +
311
+ `(marker, keyId, algorithm, iv, ciphertext), got ${parts.length}. ` +
312
+ `Value may be corrupted or not properly encrypted.`);
313
+ }
314
+ // Validate marker
315
+ if (parts[0] !== 'ENC') {
316
+ throw new Error(`Invalid encryption marker: expected "ENC", got "${parts[0]}". ` +
317
+ `Value may not be a properly encrypted MemberJunction field.`);
318
+ }
319
+ // Validate key ID looks like a UUID
320
+ if (!this.isValidUUID(parts[1])) {
321
+ throw new Error(`Invalid key ID in encrypted value: "${parts[1]}". ` +
322
+ `Expected a valid UUID.`);
323
+ }
324
+ return {
325
+ marker: global_1.ENCRYPTION_MARKER,
326
+ keyId: parts[1],
327
+ algorithm: parts[2],
328
+ iv: parts[3],
329
+ ciphertext: parts[4],
330
+ authTag: parts[5] // May be undefined for non-AEAD
331
+ };
332
+ }
333
+ /**
334
+ * Validates that key material is accessible at a given lookup value.
335
+ *
336
+ * Used before key rotation to verify the new key exists and is valid.
337
+ * This prevents starting a rotation that would fail mid-way.
338
+ *
339
+ * @param lookupValue - The key source lookup value to validate
340
+ * @param encryptionKeyId - The key ID (to get source configuration)
341
+ * @param contextUser - User context for database access
342
+ *
343
+ * @throws Error if the key material cannot be accessed or is invalid
344
+ *
345
+ * @example
346
+ * ```typescript
347
+ * // Before rotation, validate the new key is ready
348
+ * await engine.ValidateKeyMaterial(
349
+ * 'MJ_ENCRYPTION_KEY_PII_NEW',
350
+ * existingKeyId,
351
+ * contextUser
352
+ * );
353
+ * // If no error, safe to proceed with rotation
354
+ * ```
355
+ */
356
+ async ValidateKeyMaterial(lookupValue, encryptionKeyId, contextUser) {
357
+ if (!lookupValue || typeof lookupValue !== 'string') {
358
+ throw new Error('Invalid lookup value for key validation. ' +
359
+ 'Provide the environment variable name or config key for the new key.');
360
+ }
361
+ // Ensure engine is configured
362
+ await this.ensureConfigured(contextUser);
363
+ // Get the key configuration to know the source type and algorithm
364
+ const keyConfig = this.buildKeyConfiguration(encryptionKeyId);
365
+ // Get or create the key source
366
+ const source = await this.getOrCreateKeySource(keyConfig.source.driverClass);
367
+ // Try to retrieve the key from the new lookup value
368
+ const keyMaterial = await source.GetKey(lookupValue);
369
+ // Validate key length matches algorithm requirements
370
+ const expectedBytes = keyConfig.algorithm.keyLengthBits / 8;
371
+ if (keyMaterial.length !== expectedBytes) {
372
+ throw new Error(`Key length mismatch: expected ${expectedBytes} bytes for ${keyConfig.algorithm.name}, ` +
373
+ `got ${keyMaterial.length} bytes. ` +
374
+ `Generate a key with: openssl rand -base64 ${expectedBytes}`);
375
+ }
376
+ }
377
+ /**
378
+ * Encrypts a value using a specific key lookup (for key rotation).
379
+ *
380
+ * During key rotation, we need to encrypt with the new key material
381
+ * before updating the key metadata. This method allows specifying
382
+ * an alternate lookup value for the key material.
383
+ *
384
+ * @param plaintext - The value to encrypt
385
+ * @param encryptionKeyId - The key ID (for algorithm/marker config)
386
+ * @param keyLookupValue - Alternate lookup value for key material
387
+ * @param contextUser - User context for database access
388
+ * @returns The encrypted value string
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * // During rotation, encrypt with new key before metadata update
393
+ * const newEncrypted = await engine.EncryptWithLookup(
394
+ * decryptedData,
395
+ * keyId,
396
+ * 'MJ_ENCRYPTION_KEY_PII_NEW',
397
+ * contextUser
398
+ * );
399
+ * ```
400
+ */
401
+ async EncryptWithLookup(plaintext, encryptionKeyId, keyLookupValue, contextUser) {
402
+ // Handle null/undefined
403
+ if (plaintext === null || plaintext === undefined) {
404
+ return plaintext;
405
+ }
406
+ // Ensure engine is configured
407
+ await this.ensureConfigured(contextUser);
408
+ // Get the base configuration
409
+ const keyConfig = this.buildKeyConfiguration(encryptionKeyId);
410
+ // Get key material using the overridden lookup value
411
+ const keyMaterial = await this.getKeyMaterialWithLookup(keyConfig, keyLookupValue);
412
+ // Perform encryption with the overridden key
413
+ return this.performEncryption(plaintext, keyConfig, keyMaterial);
414
+ }
415
+ /**
416
+ * Clears key material and source caches.
417
+ *
418
+ * Call after key rotation or configuration changes to ensure
419
+ * fresh data is loaded. The base class metadata caches are
420
+ * handled separately via RefreshAllItems().
421
+ */
422
+ ClearCaches() {
423
+ this._keyMaterialCache.clear();
424
+ // Don't clear source cache - sources can be reused
425
+ }
426
+ /**
427
+ * Clears all caches including base class metadata caches.
428
+ *
429
+ * This is more aggressive than ClearCaches() and should be used
430
+ * when you need to completely refresh all cached data.
431
+ */
432
+ async ClearAllCaches() {
433
+ this._keyMaterialCache.clear();
434
+ await this.RefreshAllItems();
435
+ }
436
+ // ========================================================================
437
+ // PRIVATE METHODS
438
+ // ========================================================================
439
+ /**
440
+ * Ensures the engine is configured before operations.
441
+ *
442
+ * @private
443
+ */
444
+ async ensureConfigured(contextUser) {
445
+ if (!this.Loaded) {
446
+ await this.Config(false, contextUser);
447
+ }
448
+ }
449
+ /**
450
+ * Builds a KeyConfiguration object from the cached metadata.
451
+ *
452
+ * @private
453
+ */
454
+ buildKeyConfiguration(keyId) {
455
+ const keyConfig = this.GetKeyConfiguration(keyId);
456
+ if (!keyConfig) {
457
+ throw new Error(`Encryption key not found: ${keyId}. ` +
458
+ 'Ensure the key exists and the engine is configured.');
459
+ }
460
+ const { key, algorithm, source } = keyConfig;
461
+ // Validate key is usable
462
+ if (key.Status === 'Expired') {
463
+ throw new Error(`Encryption key "${key.Name}" has expired. ` +
464
+ 'Please rotate to a new key or update the expiration.');
465
+ }
466
+ if (!key.IsActive) {
467
+ throw new Error(`Encryption key "${key.Name}" is not active. ` +
468
+ 'Activate the key or select a different active key.');
469
+ }
470
+ if (!algorithm.IsActive) {
471
+ throw new Error(`Encryption algorithm "${algorithm.Name}" is not active. ` +
472
+ 'The key is configured to use a disabled algorithm.');
473
+ }
474
+ if (!source.IsActive) {
475
+ throw new Error(`Encryption key source "${source.Name}" is not active. ` +
476
+ 'The key is configured to use a disabled source type.');
477
+ }
478
+ return {
479
+ keyId: key.ID,
480
+ keyVersion: key.KeyVersion || '1',
481
+ marker: key.Marker || global_1.ENCRYPTION_MARKER,
482
+ algorithm: {
483
+ name: algorithm.Name,
484
+ nodeCryptoName: algorithm.NodeCryptoName,
485
+ keyLengthBits: algorithm.KeyLengthBits,
486
+ ivLengthBytes: algorithm.IVLengthBytes,
487
+ isAEAD: !!algorithm.IsAEAD
488
+ },
489
+ source: {
490
+ driverClass: source.DriverClass,
491
+ lookupValue: key.KeyLookupValue
492
+ }
493
+ };
494
+ }
495
+ /**
496
+ * Performs the actual encryption operation.
497
+ *
498
+ * @private
499
+ */
500
+ performEncryption(plaintext, keyConfig, keyMaterial) {
501
+ // Generate random IV
502
+ const iv = crypto.randomBytes(keyConfig.algorithm.ivLengthBytes);
503
+ // Create cipher options
504
+ const cipherOptions = keyConfig.algorithm.isAEAD ? { authTagLength: 16 } : undefined;
505
+ // Create cipher
506
+ const cipher = crypto.createCipheriv(keyConfig.algorithm.nodeCryptoName, keyMaterial, iv, cipherOptions);
507
+ // Convert plaintext to buffer
508
+ const data = typeof plaintext === 'string'
509
+ ? Buffer.from(plaintext, 'utf8')
510
+ : plaintext;
511
+ // Encrypt
512
+ const ciphertext = Buffer.concat([
513
+ cipher.update(data),
514
+ cipher.final()
515
+ ]);
516
+ // Build the serialized format
517
+ const parts = [
518
+ keyConfig.marker,
519
+ keyConfig.keyId,
520
+ keyConfig.algorithm.name,
521
+ iv.toString('base64'),
522
+ ciphertext.toString('base64')
523
+ ];
524
+ // Add auth tag for AEAD algorithms
525
+ if (keyConfig.algorithm.isAEAD) {
526
+ const authTag = cipher.getAuthTag();
527
+ parts.push(authTag.toString('base64'));
528
+ }
529
+ return parts.join('$');
530
+ }
531
+ /**
532
+ * Performs the actual decryption operation.
533
+ *
534
+ * @private
535
+ */
536
+ performDecryption(parsed, keyConfig, keyMaterial) {
537
+ // Decode IV from base64
538
+ const iv = Buffer.from(parsed.iv, 'base64');
539
+ // Validate IV length
540
+ if (iv.length !== keyConfig.algorithm.ivLengthBytes) {
541
+ throw new Error(`IV length mismatch: expected ${keyConfig.algorithm.ivLengthBytes} bytes, ` +
542
+ `got ${iv.length} bytes. The encrypted value may be corrupted.`);
543
+ }
544
+ // Create decipher options
545
+ const decipherOptions = keyConfig.algorithm.isAEAD ? { authTagLength: 16 } : undefined;
546
+ // Create decipher
547
+ const decipher = crypto.createDecipheriv(keyConfig.algorithm.nodeCryptoName, keyMaterial, iv, decipherOptions);
548
+ // Set auth tag for AEAD algorithms
549
+ if (keyConfig.algorithm.isAEAD) {
550
+ if (!parsed.authTag) {
551
+ throw new Error(`Missing authentication tag for ${keyConfig.algorithm.name}. ` +
552
+ `The encrypted value may be corrupted or was encrypted with a different algorithm.`);
553
+ }
554
+ const authTag = Buffer.from(parsed.authTag, 'base64');
555
+ decipher.setAuthTag(authTag);
556
+ }
557
+ // Decode ciphertext
558
+ const ciphertext = Buffer.from(parsed.ciphertext, 'base64');
559
+ // Decrypt
560
+ try {
561
+ const plaintext = Buffer.concat([
562
+ decipher.update(ciphertext),
563
+ decipher.final()
564
+ ]);
565
+ return plaintext.toString('utf8');
566
+ }
567
+ catch (err) {
568
+ // Decryption errors - could be wrong key, corrupted data, or auth tag mismatch
569
+ const message = err instanceof Error ? err.message : String(err);
570
+ if (message.includes('Unsupported state') || message.includes('auth')) {
571
+ throw new Error('Decryption failed: authentication tag mismatch. ' +
572
+ 'This could mean the data was tampered with, the wrong key was used, ' +
573
+ 'or the encrypted value is corrupted.');
574
+ }
575
+ throw new Error(`Decryption failed: ${message}. ` +
576
+ 'The key may be incorrect or the encrypted value may be corrupted.');
577
+ }
578
+ }
579
+ /**
580
+ * Gets key material from cache or source.
581
+ *
582
+ * @private
583
+ */
584
+ async getKeyMaterial(config) {
585
+ const cacheKey = `${config.keyId}:${config.keyVersion}`;
586
+ // Check cache
587
+ const cached = this._keyMaterialCache.get(cacheKey);
588
+ if (cached && cached.expiry > new Date()) {
589
+ return cached.value;
590
+ }
591
+ // Get or create the key source
592
+ const source = await this.getOrCreateKeySource(config.source.driverClass);
593
+ // Get key from source
594
+ const keyMaterial = await source.GetKey(config.source.lookupValue, config.keyVersion);
595
+ // Validate key length
596
+ this.validateKeyLength(keyMaterial, config);
597
+ // Cache it
598
+ this._keyMaterialCache.set(cacheKey, {
599
+ value: keyMaterial,
600
+ expiry: new Date(Date.now() + this._keyMaterialCacheTtlMs)
601
+ });
602
+ return keyMaterial;
603
+ }
604
+ /**
605
+ * Gets key material using an overridden lookup value (for rotation).
606
+ *
607
+ * @private
608
+ */
609
+ async getKeyMaterialWithLookup(config, lookupValue) {
610
+ // Don't cache - this is for rotation with temporary lookup values
611
+ const source = await this.getOrCreateKeySource(config.source.driverClass);
612
+ const keyMaterial = await source.GetKey(lookupValue, config.keyVersion);
613
+ this.validateKeyLength(keyMaterial, config);
614
+ return keyMaterial;
615
+ }
616
+ /**
617
+ * Gets or creates a key source instance.
618
+ *
619
+ * @private
620
+ */
621
+ async getOrCreateKeySource(driverClass) {
622
+ // Check cache
623
+ let source = this._keySourceCache.get(driverClass);
624
+ if (source) {
625
+ return source;
626
+ }
627
+ // Create new instance via ClassFactory
628
+ try {
629
+ const result = global_1.MJGlobal.Instance.ClassFactory.CreateInstance(EncryptionKeySourceBase_1.EncryptionKeySourceBase, driverClass);
630
+ if (result) {
631
+ source = result;
632
+ }
633
+ }
634
+ catch (err) {
635
+ const message = err instanceof Error ? err.message : String(err);
636
+ throw new Error(`Failed to create key source "${driverClass}": ${message}. ` +
637
+ 'Ensure the key source provider is properly registered.');
638
+ }
639
+ if (!source) {
640
+ throw new Error(`Key source "${driverClass}" not found. ` +
641
+ 'Ensure the provider class is registered with @RegisterClass.');
642
+ }
643
+ // Initialize the source
644
+ await source.Initialize();
645
+ // Validate configuration
646
+ if (!source.ValidateConfiguration()) {
647
+ (0, core_1.LogError)(`Key source "${driverClass}" configuration validation failed. ` +
648
+ 'The source may not work correctly.');
649
+ }
650
+ // Cache it
651
+ this._keySourceCache.set(driverClass, source);
652
+ return source;
653
+ }
654
+ /**
655
+ * Validates that key material has the correct length for the algorithm.
656
+ *
657
+ * @private
658
+ */
659
+ validateKeyLength(keyMaterial, config) {
660
+ const expectedBytes = config.algorithm.keyLengthBits / 8;
661
+ if (keyMaterial.length !== expectedBytes) {
662
+ throw new Error(`Key length mismatch for "${config.algorithm.name}": ` +
663
+ `expected ${expectedBytes} bytes (${config.algorithm.keyLengthBits} bits), ` +
664
+ `got ${keyMaterial.length} bytes (${keyMaterial.length * 8} bits). ` +
665
+ `Generate a correct key with: openssl rand -base64 ${expectedBytes}`);
666
+ }
667
+ }
668
+ /**
669
+ * Validates that a string is a valid UUID.
670
+ *
671
+ * @private
672
+ */
673
+ isValidUUID(value) {
674
+ if (!value || typeof value !== 'string') {
675
+ return false;
676
+ }
677
+ // Standard UUID format
678
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
679
+ return uuidPattern.test(value);
680
+ }
681
+ }
682
+ exports.EncryptionEngine = EncryptionEngine;
683
+ //# sourceMappingURL=EncryptionEngine.js.map