@memberjunction/encryption 0.0.1 → 2.129.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/README.md +391 -28
- package/dist/EncryptionEngine.d.ts +351 -0
- package/dist/EncryptionEngine.d.ts.map +1 -0
- package/dist/EncryptionEngine.js +683 -0
- package/dist/EncryptionEngine.js.map +1 -0
- package/dist/EncryptionKeySourceBase.d.ts +203 -0
- package/dist/EncryptionKeySourceBase.d.ts.map +1 -0
- package/dist/EncryptionKeySourceBase.js +133 -0
- package/dist/EncryptionKeySourceBase.js.map +1 -0
- package/dist/actions/EnableFieldEncryptionAction.d.ts +87 -0
- package/dist/actions/EnableFieldEncryptionAction.d.ts.map +1 -0
- package/dist/actions/EnableFieldEncryptionAction.js +308 -0
- package/dist/actions/EnableFieldEncryptionAction.js.map +1 -0
- package/dist/actions/RotateEncryptionKeyAction.d.ts +79 -0
- package/dist/actions/RotateEncryptionKeyAction.d.ts.map +1 -0
- package/dist/actions/RotateEncryptionKeyAction.js +343 -0
- package/dist/actions/RotateEncryptionKeyAction.js.map +1 -0
- package/dist/actions/index.d.ts +12 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +17 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +216 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +15 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/providers/AWSKMSKeySource.d.ts +110 -0
- package/dist/providers/AWSKMSKeySource.d.ts.map +1 -0
- package/dist/providers/AWSKMSKeySource.js +245 -0
- package/dist/providers/AWSKMSKeySource.js.map +1 -0
- package/dist/providers/AzureKeyVaultKeySource.d.ts +109 -0
- package/dist/providers/AzureKeyVaultKeySource.d.ts.map +1 -0
- package/dist/providers/AzureKeyVaultKeySource.js +268 -0
- package/dist/providers/AzureKeyVaultKeySource.js.map +1 -0
- package/dist/providers/ConfigFileKeySource.d.ts +173 -0
- package/dist/providers/ConfigFileKeySource.d.ts.map +1 -0
- package/dist/providers/ConfigFileKeySource.js +310 -0
- package/dist/providers/ConfigFileKeySource.js.map +1 -0
- package/dist/providers/EnvVarKeySource.d.ts +152 -0
- package/dist/providers/EnvVarKeySource.d.ts.map +1 -0
- package/dist/providers/EnvVarKeySource.js +251 -0
- package/dist/providers/EnvVarKeySource.js.map +1 -0
- 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
|