@memberjunction/encryption 3.4.0 → 4.1.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 (38) hide show
  1. package/README.md +347 -236
  2. package/dist/EncryptionEngine.d.ts +1 -2
  3. package/dist/EncryptionEngine.d.ts.map +1 -1
  4. package/dist/EncryptionEngine.js +44 -68
  5. package/dist/EncryptionEngine.js.map +1 -1
  6. package/dist/EncryptionKeySourceBase.d.ts +1 -2
  7. package/dist/EncryptionKeySourceBase.d.ts.map +1 -1
  8. package/dist/EncryptionKeySourceBase.js +1 -12
  9. package/dist/EncryptionKeySourceBase.js.map +1 -1
  10. package/dist/actions/EnableFieldEncryptionAction.js +19 -22
  11. package/dist/actions/EnableFieldEncryptionAction.js.map +1 -1
  12. package/dist/actions/RotateEncryptionKeyAction.js +20 -23
  13. package/dist/actions/RotateEncryptionKeyAction.js.map +1 -1
  14. package/dist/actions/index.d.ts +2 -2
  15. package/dist/actions/index.js +2 -7
  16. package/dist/actions/index.js.map +1 -1
  17. package/dist/index.d.ts +9 -9
  18. package/dist/index.js +8 -19
  19. package/dist/index.js.map +1 -1
  20. package/dist/interfaces.js +1 -2
  21. package/dist/interfaces.js.map +1 -1
  22. package/dist/providers/AWSKMSKeySource.d.ts +1 -2
  23. package/dist/providers/AWSKMSKeySource.d.ts.map +1 -1
  24. package/dist/providers/AWSKMSKeySource.js +15 -38
  25. package/dist/providers/AWSKMSKeySource.js.map +1 -1
  26. package/dist/providers/AzureKeyVaultKeySource.d.ts +1 -2
  27. package/dist/providers/AzureKeyVaultKeySource.d.ts.map +1 -1
  28. package/dist/providers/AzureKeyVaultKeySource.js +14 -37
  29. package/dist/providers/AzureKeyVaultKeySource.js.map +1 -1
  30. package/dist/providers/ConfigFileKeySource.d.ts +1 -2
  31. package/dist/providers/ConfigFileKeySource.d.ts.map +1 -1
  32. package/dist/providers/ConfigFileKeySource.js +23 -25
  33. package/dist/providers/ConfigFileKeySource.js.map +1 -1
  34. package/dist/providers/EnvVarKeySource.d.ts +1 -2
  35. package/dist/providers/EnvVarKeySource.d.ts.map +1 -1
  36. package/dist/providers/EnvVarKeySource.js +6 -9
  37. package/dist/providers/EnvVarKeySource.js.map +1 -1
  38. package/package.json +16 -15
package/README.md CHANGED
@@ -1,15 +1,6 @@
1
1
  # @memberjunction/encryption
2
2
 
3
- Comprehensive and general purpose encryption package. Used for field-level encryption for MemberJunction entities. Field-level encryption provides transparent encrypt-on-save and decrypt-on-load operations, configurable per field via entity metadata. This package can be used for any other use-cases where encryption/decryption is required.
4
-
5
- ## Features
6
-
7
- - **AES-256-GCM Encryption** - Industry-standard authenticated encryption (AEAD) that prevents tampering
8
- - **Pluggable Key Sources** - Environment variables, config files, or custom providers (vault services, cloud KMS)
9
- - **Declarative Configuration** - Enable encryption via EntityField metadata without code changes
10
- - **Transparent Operation** - Automatic encryption on save, decryption on load
11
- - **Key Rotation Support** - Full re-encryption with transactional safety
12
- - **Secure Defaults** - API responses hide encrypted fields by default
3
+ Server-side field-level encryption engine for MemberJunction with pluggable key sources. This package provides transparent encrypt-on-save and decrypt-on-load operations for entity fields, configurable entirely through database metadata. It supports AES-256-GCM authenticated encryption, multiple key source backends (environment variables, configuration files, AWS KMS, Azure Key Vault), and full key rotation with transactional safety.
13
4
 
14
5
  ## Installation
15
6
 
@@ -17,14 +8,84 @@ Comprehensive and general purpose encryption package. Used for field-level encry
17
8
  npm install @memberjunction/encryption
18
9
  ```
19
10
 
11
+ For cloud key management, install the optional provider dependencies:
12
+
13
+ ```bash
14
+ # AWS KMS support
15
+ npm install @aws-sdk/client-kms
16
+
17
+ # Azure Key Vault support
18
+ npm install @azure/keyvault-secrets @azure/identity
19
+ ```
20
+
21
+ ## Overview
22
+
23
+ The encryption package sits between MemberJunction's entity system and the database, intercepting save and load operations on fields marked for encryption. When a field has `Encrypt = true` in its EntityField metadata, the engine automatically encrypts the value before writing to the database and decrypts it when reading, providing application-level transparency.
24
+
25
+ The system is designed around three database-driven configuration entities -- Encryption Keys, Encryption Algorithms, and Encryption Key Sources -- which together define *what* key material to use, *which* algorithm to apply, and *where* to retrieve the raw key bytes from. This metadata-driven approach means encryption can be enabled or disabled on individual fields without code changes.
26
+
27
+ ```mermaid
28
+ flowchart TD
29
+ subgraph App["Application Layer"]
30
+ Entity["Entity Save/Load"]
31
+ end
32
+
33
+ subgraph Engine["EncryptionEngine"]
34
+ Encrypt["Encrypt()"]
35
+ Decrypt["Decrypt()"]
36
+ Cache["Key Material Cache\n5-min TTL"]
37
+ end
38
+
39
+ subgraph Sources["Key Source Providers"]
40
+ ENV["EnvVarKeySource"]
41
+ CFG["ConfigFileKeySource"]
42
+ AWS["AWSKMSKeySource"]
43
+ AZR["AzureKeyVaultKeySource"]
44
+ CUST["Custom Provider"]
45
+ end
46
+
47
+ subgraph DB["Database"]
48
+ Meta["Encryption Metadata\nKeys / Algorithms / Sources"]
49
+ Data["Encrypted Field Data\n$ENC$..."]
50
+ end
51
+
52
+ Entity --> Encrypt
53
+ Entity --> Decrypt
54
+ Encrypt --> Cache
55
+ Decrypt --> Cache
56
+ Cache --> ENV
57
+ Cache --> CFG
58
+ Cache --> AWS
59
+ Cache --> AZR
60
+ Cache --> CUST
61
+ Encrypt --> Data
62
+ Decrypt --> Data
63
+ Engine --> Meta
64
+
65
+ style App fill:#2d6a9f,stroke:#1a4971,color:#fff
66
+ style Engine fill:#7c5295,stroke:#563a6b,color:#fff
67
+ style Sources fill:#2d8659,stroke:#1a5c3a,color:#fff
68
+ style DB fill:#b8762f,stroke:#8a5722,color:#fff
69
+ ```
70
+
71
+ ## Key Features
72
+
73
+ - **AES-256-GCM Encryption** -- Industry-standard authenticated encryption (AEAD) that prevents both eavesdropping and tampering
74
+ - **Pluggable Key Sources** -- Environment variables, config files, AWS KMS, Azure Key Vault, or custom providers via the ClassFactory pattern
75
+ - **Declarative Configuration** -- Enable encryption on any entity field via database metadata without code changes
76
+ - **Transparent Operation** -- Automatic encryption on save and decryption on load
77
+ - **Key Rotation Support** -- Full re-encryption with transactional safety, batch processing, and progress tracking
78
+ - **Secure Defaults** -- API responses hide encrypted fields by default; plaintext must be explicitly opted into
79
+ - **Self-Describing Format** -- Encrypted values embed the key ID, algorithm, IV, ciphertext, and auth tag for algorithm-agnostic decryption
80
+ - **Multi-Level Caching** -- Key configurations and key material are cached with configurable TTL for performance
81
+
20
82
  ## Quick Start
21
83
 
22
- ### 1. Set Up Encryption Key
84
+ ### 1. Set Up an Encryption Key
23
85
 
24
- Create a 256-bit (32 byte) encryption key:
86
+ Generate a 256-bit (32-byte) encryption key:
25
87
 
26
88
  ```bash
27
- # Generate a secure key
28
89
  openssl rand -base64 32
29
90
  ```
30
91
 
@@ -34,12 +95,11 @@ Store it in an environment variable:
34
95
  export MJ_ENCRYPTION_KEY_PII=your-base64-key-here
35
96
  ```
36
97
 
37
- ### 2. Configure the Encryption Key in Database
98
+ ### 2. Register the Key in the Database
38
99
 
39
- Run the migration to create encryption infrastructure, then register your key:
100
+ After running the encryption migration, register your key:
40
101
 
41
102
  ```sql
42
- -- Insert your encryption key (after running the migration)
43
103
  INSERT INTO [${flyway:defaultSchema}].[EncryptionKey] (
44
104
  ID, Name, Description, EncryptionKeySourceID, EncryptionAlgorithmID,
45
105
  KeyLookupValue, KeyVersion, Marker, IsActive, Status, ActivatedAt
@@ -61,7 +121,7 @@ VALUES (
61
121
 
62
122
  ### 3. Enable Encryption on Entity Fields
63
123
 
64
- Update the EntityField metadata to enable encryption:
124
+ Update EntityField metadata to enable encryption:
65
125
 
66
126
  ```sql
67
127
  UPDATE [${flyway:defaultSchema}].[EntityField]
@@ -75,7 +135,7 @@ WHERE Entity = 'Contacts'
75
135
 
76
136
  ### 4. Encrypt Existing Data
77
137
 
78
- After enabling encryption on a field, run the EnableFieldEncryption action:
138
+ After enabling encryption on a field, run the action to encrypt existing plaintext data:
79
139
 
80
140
  ```typescript
81
141
  import { EnableFieldEncryptionAction } from '@memberjunction/encryption';
@@ -90,21 +150,137 @@ const result = await action.Run({
90
150
  });
91
151
  ```
92
152
 
153
+ ## Architecture
154
+
155
+ ### Class Hierarchy
156
+
157
+ ```mermaid
158
+ classDiagram
159
+ class BaseEngine {
160
+ +Config()
161
+ +Load()
162
+ +Loaded: boolean
163
+ }
164
+
165
+ class EncryptionEngineBase {
166
+ +EncryptionKeys: EncryptionKeyEntity[]
167
+ +EncryptionAlgorithms: EncryptionAlgorithmEntity[]
168
+ +EncryptionKeySources: EncryptionKeySourceEntity[]
169
+ +GetKeyByID(keyId)
170
+ +GetKeyConfiguration(keyId)
171
+ +ValidateKey(keyId)
172
+ }
173
+
174
+ class EncryptionEngine {
175
+ +Instance: EncryptionEngine
176
+ +Encrypt(plaintext, keyId, user)
177
+ +Decrypt(value, user)
178
+ +IsEncrypted(value)
179
+ +ParseEncryptedValue(value)
180
+ +ValidateKeyMaterial(lookup, keyId, user)
181
+ +EncryptWithLookup(plaintext, keyId, lookup, user)
182
+ +ClearCaches()
183
+ }
184
+
185
+ class EncryptionKeySourceBase {
186
+ +SourceName: string
187
+ +ValidateConfiguration()
188
+ +GetKey(lookupValue, version)
189
+ +KeyExists(lookupValue)
190
+ +Initialize()
191
+ +Dispose()
192
+ }
193
+
194
+ class EnvVarKeySource
195
+ class ConfigFileKeySource
196
+ class AWSKMSKeySource
197
+ class AzureKeyVaultKeySource
198
+
199
+ BaseEngine <|-- EncryptionEngineBase
200
+ EncryptionEngineBase <|-- EncryptionEngine
201
+ EncryptionKeySourceBase <|-- EnvVarKeySource
202
+ EncryptionKeySourceBase <|-- ConfigFileKeySource
203
+ EncryptionKeySourceBase <|-- AWSKMSKeySource
204
+ EncryptionKeySourceBase <|-- AzureKeyVaultKeySource
205
+
206
+ EncryptionEngine --> EncryptionKeySourceBase : resolves via ClassFactory
207
+
208
+ style EncryptionEngine fill:#7c5295,stroke:#563a6b,color:#fff
209
+ style EncryptionEngineBase fill:#2d6a9f,stroke:#1a4971,color:#fff
210
+ style EncryptionKeySourceBase fill:#2d8659,stroke:#1a5c3a,color:#fff
211
+ ```
212
+
213
+ The `EncryptionEngineBase` (defined in `@memberjunction/core-entities`) provides metadata caching for encryption keys, algorithms, and key sources. It works in both client and server contexts. The `EncryptionEngine` in this package extends it with actual cryptographic operations using Node.js `crypto`, making it server-side only.
214
+
215
+ ### Encrypted Value Format
216
+
217
+ Encrypted values are stored as self-describing strings that embed everything needed for decryption:
218
+
219
+ ```
220
+ $ENC$<keyId>$<algorithm>$<iv>$<ciphertext>$<authTag>
221
+ ```
222
+
223
+ For example:
224
+
225
+ ```
226
+ $ENC$550e8400-e29b-41d4-a716-446655440000$AES-256-GCM$Base64IV$Base64Ciphertext$Base64AuthTag
227
+ ```
228
+
229
+ This format enables:
230
+ - Quick detection of encrypted values via the `$ENC$` marker
231
+ - Identification of which key was used (for multi-key environments)
232
+ - Algorithm-agnostic decryption
233
+ - Key rotation without format changes
234
+
235
+ ### Encryption and Decryption Flow
236
+
237
+ ```mermaid
238
+ sequenceDiagram
239
+ participant App as Application
240
+ participant EE as EncryptionEngine
241
+ participant Cache as Key Cache
242
+ participant KS as Key Source
243
+ participant Crypto as Node.js crypto
244
+
245
+ Note over App,Crypto: Encryption Flow
246
+ App->>EE: Encrypt(plaintext, keyId, user)
247
+ EE->>EE: buildKeyConfiguration(keyId)
248
+ EE->>Cache: Check key material cache
249
+ alt Cache miss
250
+ Cache->>KS: GetKey(lookupValue, version)
251
+ KS-->>Cache: Buffer (raw key bytes)
252
+ end
253
+ Cache-->>EE: Key material (Buffer)
254
+ EE->>Crypto: createCipheriv(algo, key, randomIV)
255
+ Crypto-->>EE: Ciphertext + Auth Tag
256
+ EE-->>App: $ENC$keyId$algo$iv$ciphertext$authTag
257
+
258
+ Note over App,Crypto: Decryption Flow
259
+ App->>EE: Decrypt(encryptedValue, user)
260
+ EE->>EE: ParseEncryptedValue(value)
261
+ EE->>EE: buildKeyConfiguration(parsed.keyId)
262
+ EE->>Cache: Check key material cache
263
+ Cache-->>EE: Key material (Buffer)
264
+ EE->>Crypto: createDecipheriv(algo, key, iv)
265
+ Crypto-->>EE: Plaintext
266
+ EE-->>App: Decrypted string
267
+ ```
268
+
93
269
  ## API Response Behavior
94
270
 
95
- The encryption system provides secure-by-default API responses:
271
+ The encryption system provides secure-by-default API responses controlled by two EntityField flags:
96
272
 
97
273
  | AllowDecryptInAPI | SendEncryptedValue | API Response |
98
- |-------------------|-------------------|--------------|
99
- | true | N/A | Decrypted plaintext |
100
- | false | true | Encrypted ciphertext ($ENC$...) |
101
- | false | false | NULL (most secure, **default**) |
274
+ |---|---|---|
275
+ | `true` | N/A | Decrypted plaintext |
276
+ | `false` | `true` | Encrypted ciphertext (`$ENC$...`) |
277
+ | `false` | `false` | `NULL` (most secure, **default**) |
102
278
 
103
279
  ## Key Source Providers
104
280
 
105
281
  ### Environment Variable (Default)
106
282
 
107
- The simplest option - store keys in environment variables:
283
+ The simplest option -- store keys in environment variables. Best for development and containerized deployments with secret injection.
108
284
 
109
285
  ```bash
110
286
  # Generate a 256-bit key
@@ -114,10 +290,12 @@ openssl rand -base64 32
114
290
  export MJ_ENCRYPTION_KEY_PII=your-base64-key-here
115
291
  ```
116
292
 
117
- Database configuration:
293
+ **Database configuration:**
118
294
  - **EncryptionKeySourceID**: `38A961D2-022B-49C2-919F-1825A0E9C6F9`
119
295
  - **KeyLookupValue**: Environment variable name (e.g., `MJ_ENCRYPTION_KEY_PII`)
120
296
 
297
+ For versioned keys (during rotation), the provider appends `_V{version}` to the variable name (e.g., `MJ_ENCRYPTION_KEY_PII_V2` for version 2).
298
+
121
299
  ### Configuration File
122
300
 
123
301
  Store keys in `mj.config.cjs` (not recommended for production):
@@ -130,17 +308,15 @@ module.exports = {
130
308
  };
131
309
  ```
132
310
 
133
- Database configuration:
311
+ **Database configuration:**
134
312
  - **EncryptionKeySourceID**: `CBF9632D-EF05-42E2-82F6-5BAC79FAA565`
135
313
  - **KeyLookupValue**: Key name in config (e.g., `pii_master_key`)
136
314
 
137
- ### AWS KMS
315
+ Uses cosmiconfig to locate configuration files in standard locations (`mj.config.cjs`, `mj.config.js`, `.mjrc.json`, `.mjrc.yaml`).
138
316
 
139
- Uses AWS Key Management Service with envelope encryption. Install the optional dependency:
317
+ ### AWS KMS
140
318
 
141
- ```bash
142
- npm install @aws-sdk/client-kms
143
- ```
319
+ Uses AWS Key Management Service with envelope encryption. The raw key is encrypted by a KMS Customer Master Key (CMK) and decrypted at runtime.
144
320
 
145
321
  **Setup:**
146
322
 
@@ -155,88 +331,43 @@ npm install @aws-sdk/client-kms
155
331
  ```
156
332
  3. Store the output (base64 CiphertextBlob) as the KeyLookupValue
157
333
 
158
- **Authentication:** Uses the standard AWS credential chain:
159
- - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
160
- - IAM role (on EC2, ECS, Lambda)
161
- - Shared credentials file
334
+ **Authentication:** Uses the standard AWS credential chain (environment variables, IAM role, shared credentials file).
162
335
 
163
- Database configuration:
336
+ **Database configuration:**
164
337
  - **EncryptionKeySourceID**: `D8E4F521-3A7B-4C9E-8F12-6B5A4C3D2E1F`
165
338
  - **KeyLookupValue**: Base64-encoded CiphertextBlob from GenerateDataKey
166
339
 
167
- ```sql
168
- INSERT INTO [${flyway:defaultSchema}].[EncryptionKey] (
169
- ID, Name, EncryptionKeySourceID, EncryptionAlgorithmID,
170
- KeyLookupValue, IsActive, Status
171
- )
172
- VALUES (
173
- NEWID(),
174
- 'AWS KMS PII Key',
175
- 'D8E4F521-3A7B-4C9E-8F12-6B5A4C3D2E1F', -- AWS KMS
176
- 'B2E88E95-D09B-4DA6-B0AE-511B21B70952', -- AES-256-GCM
177
- 'AQIDAHh...base64-ciphertext-blob...', -- From GenerateDataKey
178
- 1,
179
- 'Active'
180
- );
181
- ```
182
-
183
340
  ### Azure Key Vault
184
341
 
185
- Retrieves keys from Azure Key Vault secrets. Install the optional dependencies:
186
-
187
- ```bash
188
- npm install @azure/keyvault-secrets @azure/identity
189
- ```
342
+ Retrieves keys from Azure Key Vault secrets.
190
343
 
191
344
  **Setup:**
192
345
 
193
346
  1. Create an Azure Key Vault
194
347
  2. Create a secret containing your base64-encoded key:
195
348
  ```bash
196
- # Generate key
197
349
  KEY=$(openssl rand -base64 32)
198
-
199
- # Store in Key Vault
200
350
  az keyvault secret set \
201
351
  --vault-name your-vault-name \
202
352
  --name mj-encryption-key \
203
353
  --value "$KEY"
204
354
  ```
205
355
 
206
- **Authentication:** Uses DefaultAzureCredential:
207
- - Managed Identity (on Azure VMs, App Service, Functions)
208
- - Service principal (`AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`)
209
- - Azure CLI credentials
356
+ **Authentication:** Uses DefaultAzureCredential (Managed Identity, service principal, or Azure CLI).
210
357
 
211
- Database configuration:
358
+ **Database configuration:**
212
359
  - **EncryptionKeySourceID**: `A2B3C4D5-E6F7-8901-2345-6789ABCDEF01`
213
- - **KeyLookupValue**: Full secret URL or secret name (if `AZURE_KEYVAULT_URL` is set)
360
+ - **KeyLookupValue**: Full secret URL or just the secret name (if `AZURE_KEYVAULT_URL` is set)
214
361
 
215
- ```sql
216
- INSERT INTO [${flyway:defaultSchema}].[EncryptionKey] (
217
- ID, Name, EncryptionKeySourceID, EncryptionAlgorithmID,
218
- KeyLookupValue, IsActive, Status
219
- )
220
- VALUES (
221
- NEWID(),
222
- 'Azure Key Vault PII Key',
223
- 'A2B3C4D5-E6F7-8901-2345-6789ABCDEF01', -- Azure Key Vault
224
- 'B2E88E95-D09B-4DA6-B0AE-511B21B70952', -- AES-256-GCM
225
- 'https://your-vault.vault.azure.net/secrets/mj-encryption-key',
226
- 1,
227
- 'Active'
228
- );
229
- ```
230
-
231
- **Tip:** Set `AZURE_KEYVAULT_URL` to use short secret names:
232
362
  ```bash
363
+ # With AZURE_KEYVAULT_URL set, use short names:
233
364
  export AZURE_KEYVAULT_URL=https://your-vault.vault.azure.net
234
- # Then KeyLookupValue can just be: mj-encryption-key
365
+ # Then KeyLookupValue can be: mj-encryption-key
235
366
  ```
236
367
 
237
368
  ### Custom Provider
238
369
 
239
- Extend `EncryptionKeySourceBase` for other vault services:
370
+ Extend `EncryptionKeySourceBase` to integrate any key management system:
240
371
 
241
372
  ```typescript
242
373
  import { RegisterClass } from '@memberjunction/global';
@@ -261,36 +392,14 @@ export class HashiCorpVaultKeySource extends EncryptionKeySourceBase {
261
392
  }
262
393
  ```
263
394
 
264
- ## Key Rotation
265
-
266
- Rotate keys without downtime using the RotateEncryptionKey action:
267
-
268
- ```typescript
269
- import { RotateEncryptionKeyAction } from '@memberjunction/encryption';
270
-
271
- // 1. Deploy new key to environment
272
- // export MJ_ENCRYPTION_KEY_PII_V2=new-base64-key-here
273
-
274
- // 2. Run rotation
275
- const action = new RotateEncryptionKeyAction();
276
- const result = await action.Run({
277
- Params: [
278
- { Name: 'EncryptionKeyID', Value: 'existing-key-uuid' },
279
- { Name: 'NewKeyLookupValue', Value: 'MJ_ENCRYPTION_KEY_PII_V2' },
280
- { Name: 'BatchSize', Value: 100 }
281
- ],
282
- ContextUser: currentUser
283
- });
284
-
285
- // 3. After rotation, update environment to use new key
286
- // export MJ_ENCRYPTION_KEY_PII=new-key-value
287
- // Remove MJ_ENCRYPTION_KEY_PII_V2
288
- ```
395
+ The provider lifecycle is: **Construction** -> **Initialize()** (async setup) -> **GetKey()/KeyExists()** (per-operation) -> **Dispose()** (cleanup).
289
396
 
290
397
  ## Programmatic API
291
398
 
292
399
  ### EncryptionEngine
293
400
 
401
+ The `EncryptionEngine` is a singleton accessed via `EncryptionEngine.Instance`:
402
+
294
403
  ```typescript
295
404
  import { EncryptionEngine } from '@memberjunction/encryption';
296
405
 
@@ -303,7 +412,7 @@ const encrypted = await engine.Encrypt(
303
412
  contextUser
304
413
  );
305
414
 
306
- // Decrypt a value
415
+ // Decrypt a value (non-encrypted values pass through unchanged)
307
416
  const decrypted = await engine.Decrypt(encrypted, contextUser);
308
417
 
309
418
  // Check if a value is encrypted
@@ -312,183 +421,185 @@ if (engine.IsEncrypted(someValue)) {
312
421
  console.log(`Encrypted with key: ${parts.keyId}`);
313
422
  }
314
423
 
315
- // Clear caches (after key rotation)
424
+ // Clear caches (after key rotation or config changes)
316
425
  engine.ClearCaches();
317
426
  ```
318
427
 
319
- ## API Key Management
428
+ ### Key Rotation
320
429
 
321
- The `EncryptionEngine` provides secure API key management for authentication scenarios like MCP servers, external integrations, and programmatic access.
322
-
323
- ### Creating API Keys
430
+ Rotate keys without downtime using the `RotateEncryptionKeyAction`:
324
431
 
325
432
  ```typescript
326
- import { EncryptionEngine } from '@memberjunction/encryption';
327
-
328
- const result = await EncryptionEngine.Instance.CreateAPIKey({
329
- userId: 'user-guid-here',
330
- label: 'MCP Server Integration',
331
- description: 'Used for Claude Desktop MCP connections',
332
- expiresAt: new Date('2025-12-31') // Optional - omit for non-expiring keys
333
- }, contextUser);
334
-
335
- if (result.success) {
336
- // CRITICAL: Save this key immediately - it cannot be recovered!
337
- console.log('Your API Key:', result.rawKey);
338
- console.log('API Key ID:', result.apiKeyId);
339
- } else {
340
- console.error('Failed to create API key:', result.error);
341
- }
342
- ```
343
-
344
- **Key Format**: `mj_sk_[64 hex characters]` (70 characters total)
345
-
346
- **Security**: Only the SHA-256 hash is stored in the database. The raw key is returned exactly once at creation time and cannot be recovered.
433
+ import { RotateEncryptionKeyAction } from '@memberjunction/encryption';
347
434
 
348
- ### Validating API Keys
435
+ // 1. Deploy new key to environment
436
+ // export MJ_ENCRYPTION_KEY_PII_V2=new-base64-key-here
349
437
 
350
- ```typescript
351
- const validation = await EncryptionEngine.Instance.ValidateAPIKey(
352
- request.headers['x-api-key'],
353
- systemUser
354
- );
438
+ // 2. Run rotation
439
+ const action = new RotateEncryptionKeyAction();
440
+ const result = await action.Run({
441
+ Params: [
442
+ { Name: 'EncryptionKeyID', Value: 'existing-key-uuid' },
443
+ { Name: 'NewKeyLookupValue', Value: 'MJ_ENCRYPTION_KEY_PII_V2' },
444
+ { Name: 'BatchSize', Value: 100 }
445
+ ],
446
+ ContextUser: currentUser
447
+ });
355
448
 
356
- if (validation.isValid) {
357
- // Use validation.user for authorized operations
358
- console.log('Authenticated user:', validation.user.Name);
359
- console.log('API Key ID:', validation.apiKeyId);
360
- } else {
361
- throw new Error(validation.error);
362
- }
449
+ // 3. After rotation completes, update environment to use new key
363
450
  ```
364
451
 
365
- The validation method:
366
- - Checks key format
367
- - Looks up the hash in the CredentialEngine cache (fast!)
368
- - Verifies the key is active and not expired
369
- - Loads the associated user from the database
370
- - Updates `LastUsedAt` and logs usage
371
-
372
- ### Other API Key Methods
373
-
374
- ```typescript
375
- // Generate a key without storing it (for custom storage scenarios)
376
- const { raw, hash } = EncryptionEngine.Instance.GenerateAPIKey();
377
-
378
- // Hash a key for manual comparison
379
- const keyHash = EncryptionEngine.Instance.HashAPIKey(rawKey);
452
+ The rotation process:
453
+
454
+ ```mermaid
455
+ flowchart TD
456
+ A["Validate new key\nis accessible"] --> B["Set key status\nto 'Rotating'"]
457
+ B --> C["Find all fields\nusing this key"]
458
+ C --> D["For each field:\nLoad records in batches"]
459
+ D --> E["Decrypt with\nold key"]
460
+ E --> F["Re-encrypt with\nnew key"]
461
+ F --> G["Save updated\nrecord"]
462
+ G --> H{More records?}
463
+ H -- Yes --> D
464
+ H -- No --> I["Update key metadata\nLookupValue + Version"]
465
+ I --> J["Set status\nback to 'Active'"]
466
+ J --> K["Clear engine\ncaches"]
467
+
468
+ style A fill:#2d6a9f,stroke:#1a4971,color:#fff
469
+ style B fill:#b8762f,stroke:#8a5722,color:#fff
470
+ style I fill:#b8762f,stroke:#8a5722,color:#fff
471
+ style J fill:#2d8659,stroke:#1a5c3a,color:#fff
472
+ style K fill:#2d8659,stroke:#1a5c3a,color:#fff
473
+ ```
380
474
 
381
- // Validate key format before processing
382
- if (!EncryptionEngine.Instance.IsValidAPIKeyFormat(key)) {
383
- throw new Error('Invalid API key format');
384
- }
475
+ ## API Reference
385
476
 
386
- // Revoke an API key (permanently disables it)
387
- const revoked = await EncryptionEngine.Instance.RevokeAPIKey(apiKeyId, contextUser);
388
- ```
477
+ ### EncryptionEngine
389
478
 
390
- ### API Key Database Schema
479
+ | Method | Description |
480
+ |--------|-------------|
481
+ | `Instance` | Static property returning the singleton instance |
482
+ | `Config(forceRefresh?, contextUser?, provider?)` | Loads encryption metadata from the database |
483
+ | `Encrypt(plaintext, encryptionKeyId, contextUser?)` | Encrypts a value using the specified key |
484
+ | `Decrypt(value, contextUser?)` | Decrypts an encrypted value; passes through non-encrypted values |
485
+ | `IsEncrypted(value, marker?)` | Checks if a value is encrypted (synchronous) |
486
+ | `ParseEncryptedValue(value)` | Parses an encrypted string into its component parts |
487
+ | `ValidateKeyMaterial(lookupValue, keyId, contextUser?)` | Validates that key material is accessible and the correct length |
488
+ | `EncryptWithLookup(plaintext, keyId, lookupValue, contextUser?)` | Encrypts using a specific key lookup value (used during rotation) |
489
+ | `ClearCaches()` | Clears the key material cache |
490
+ | `ClearAllCaches()` | Clears all caches including base class metadata |
491
+
492
+ ### EncryptionKeySourceBase
493
+
494
+ | Member | Description |
495
+ |--------|-------------|
496
+ | `SourceName` | Abstract property returning the human-readable source name |
497
+ | `ValidateConfiguration()` | Abstract method to validate source configuration |
498
+ | `GetKey(lookupValue, keyVersion?)` | Abstract method to retrieve raw key bytes |
499
+ | `KeyExists(lookupValue)` | Abstract method to check if a key exists |
500
+ | `Initialize()` | Virtual async method for one-time setup (default: no-op) |
501
+ | `Dispose()` | Virtual async method for cleanup (default: no-op) |
502
+
503
+ ### Interfaces
504
+
505
+ | Interface | Description |
506
+ |-----------|-------------|
507
+ | `EncryptedValueParts` | Parsed components of an encrypted value string (marker, keyId, algorithm, iv, ciphertext, authTag) |
508
+ | `KeyConfiguration` | Complete runtime key configuration (key ID, version, marker, algorithm details, source details) |
509
+ | `EncryptionKeySourceConfig` | Configuration passed to key source providers (lookupValue, additionalConfig) |
510
+ | `RotateKeyParams` / `RotateKeyResult` | Parameters and results for key rotation operations |
511
+ | `EnableFieldEncryptionParams` / `EnableFieldEncryptionResult` | Parameters and results for field encryption operations |
512
+
513
+ ### Actions
514
+
515
+ | Action | Registered Name | Description |
516
+ |--------|----------------|-------------|
517
+ | `EnableFieldEncryptionAction` | `Enable Field Encryption` | Encrypts existing plaintext data on a newly-encrypted field |
518
+ | `RotateEncryptionKeyAction` | `Rotate Encryption Key` | Re-encrypts all data from an old key to a new key |
391
519
 
392
- The API key system uses these tables (part of MemberJunction core):
520
+ ## Database Schema
393
521
 
394
- **MJ: API Keys**
395
- - `ID` - Unique identifier
396
- - `Hash` - SHA-256 hash of the raw key
397
- - `UserID` - Associated user (operations execute with this user's permissions)
398
- - `Label` - Friendly name
399
- - `Status` - `Active` or `Revoked`
400
- - `ExpiresAt` - Optional expiration date
401
- - `LastUsedAt` - Automatically updated on each use
522
+ The encryption infrastructure uses three metadata entities plus extensions to EntityField:
402
523
 
403
- **MJ: API Key Usage Logs**
404
- - Tracks API key usage for analytics and security monitoring
524
+ **MJ: Encryption Key Sources** -- Where keys are stored (env vars, config files, vaults)
405
525
 
406
- ## Encrypted Value Format
526
+ **MJ: Encryption Algorithms** -- Available algorithms (AES-256-GCM, etc.) with Node.js crypto identifiers
407
527
 
408
- Encrypted values are stored as self-describing strings:
528
+ **MJ: Encryption Keys** -- Configured keys linking a source and algorithm together
409
529
 
410
- ```
411
- $ENC$<keyId>$<algorithm>$<iv>$<ciphertext>$<authTag>
412
- ```
530
+ **EntityField extensions:**
531
+ - `Encrypt` -- Enable encryption for this field
532
+ - `EncryptionKeyID` -- Which key to use
533
+ - `AllowDecryptInAPI` -- Whether to return plaintext in API responses
534
+ - `SendEncryptedValue` -- Whether to return ciphertext when decryption is not allowed
413
535
 
414
- Example:
415
- ```
416
- $ENC$550e8400-e29b-41d4-a716-446655440000$AES-256-GCM$Base64IV$Base64Ciphertext$Base64AuthTag
417
- ```
536
+ ## Performance
418
537
 
419
- This format allows:
420
- - Quick detection of encrypted values
421
- - Identification of which key was used
422
- - Algorithm-agnostic decryption
423
- - Future-proof key rotation
538
+ - Key configurations are cached via `BaseEngine` with auto-refresh on entity changes
539
+ - Key material is cached with a 5-minute TTL
540
+ - Encryption and decryption use Node.js native `crypto` module (hardware-accelerated where available)
541
+ - Batch processing for key rotation and initial encryption (configurable batch size)
542
+ - Lazy loading -- the encryption engine is only activated when needed
543
+ - Cloud providers (AWS KMS, Azure Key Vault) use lazy SDK loading to avoid import cost when not used
424
544
 
425
545
  ## Security Considerations
426
546
 
427
547
  1. **Key Management**
428
- - Never store keys in the database
429
- - Use environment variables or secure vault services
548
+ - Never store keys in the database -- use environment variables or secure vault services
430
549
  - Rotate keys regularly (recommended: annually)
431
550
  - Generate keys with `openssl rand -base64 32`
432
551
 
433
552
  2. **Authenticated Encryption**
434
553
  - AES-256-GCM provides both confidentiality and integrity
435
554
  - Auth tag prevents tampering with ciphertext
436
- - Random IVs prevent pattern analysis
555
+ - Random IVs for each encryption operation prevent pattern analysis
437
556
 
438
557
  3. **API Security**
439
- - Default: encrypted fields return `null` to clients
558
+ - Default: encrypted fields return `null` to API clients
440
559
  - Explicitly enable `AllowDecryptInAPI` only when needed
441
- - Consider using `SendEncryptedValue` for client-side decryption scenarios
560
+ - Use `SendEncryptedValue` for client-side decryption scenarios
442
561
 
443
562
  4. **Key Rotation**
444
563
  - Plan for rotation before key compromise
445
- - Test rotation in staging environment first
564
+ - Test rotation in a staging environment first
446
565
  - Monitor rotation progress for large datasets
447
566
  - Keep old keys accessible until rotation completes
448
-
449
- ## Database Schema
450
-
451
- The encryption infrastructure includes three new tables:
452
-
453
- - **MJ: Encryption Key Sources** - Where keys come from (env vars, config, vaults)
454
- - **MJ: Encryption Algorithms** - Available algorithms (AES-256-GCM, etc.)
455
- - **MJ: Encryption Keys** - Configured keys linking sources and algorithms
456
-
457
- EntityField extensions:
458
- - **Encrypt** - Enable encryption for this field
459
- - **EncryptionKeyID** - Which key to use
460
- - **AllowDecryptInAPI** - Whether to decrypt in API responses
461
- - **SendEncryptedValue** - Send ciphertext when decryption not allowed
462
-
463
- ## Performance
464
-
465
- - Key configurations are cached with 5-minute TTL
466
- - Key material is cached with 5-minute TTL
467
- - Encryption/decryption uses Node.js native crypto (fast)
468
- - Batch processing for key rotation and initial encryption
469
- - Lazy loading - encryption engine only activated when needed
567
+ - Key status is set to `Rotating` during the operation for visibility
470
568
 
471
569
  ## Troubleshooting
472
570
 
473
571
  ### "Encryption key not found"
474
- - Check that the key exists in `MJ: Encryption Keys` table
475
- - Verify `IsActive = 1` and `Status = 'Active'`
476
- - Check that the referenced algorithm and source are also active
572
+ - Verify the key exists in the `MJ: Encryption Keys` table
573
+ - Check that `IsActive = 1` and `Status = 'Active'`
574
+ - Ensure the referenced algorithm and source are also active
477
575
 
478
576
  ### "Key length mismatch"
479
577
  - Ensure your key is exactly 32 bytes (256 bits) for AES-256
480
578
  - Generate with: `openssl rand -base64 32`
481
- - The base64 string should be ~44 characters
579
+ - The base64 string should be approximately 44 characters
482
580
 
483
- ### "Failed to decrypt"
484
- - The key may have been rotated - check KeyVersion
581
+ ### "Failed to decrypt" / Auth tag mismatch
582
+ - The key may have been rotated -- check `KeyVersion`
485
583
  - The data may be corrupted
486
- - Auth tag mismatch indicates tampering
584
+ - Auth tag mismatch indicates the data was tampered with or the wrong key was used
487
585
 
488
586
  ### API returns null for encrypted fields
489
- - Check `AllowDecryptInAPI` flag on the EntityField
490
- - Default is `false` for security
491
- - Update to `true` if API clients need plaintext
587
+ - Check the `AllowDecryptInAPI` flag on the EntityField
588
+ - The default is `false` for security
589
+ - Set to `true` if API clients need plaintext
590
+
591
+ ## Dependencies
592
+
593
+ This package depends on:
594
+ - [@memberjunction/global](../MJGlobal/README.md) -- Class registration and the `ENCRYPTION_MARKER` constant
595
+ - [@memberjunction/core](../MJCore/README.md) -- `BaseEngine`, `RunView`, `Metadata`, `UserInfo`
596
+ - [@memberjunction/core-entities](../MJCoreEntities/README.md) -- `EncryptionEngineBase` and encryption entity types
597
+ - [@memberjunction/credentials](../Credentials/Engine/README.md) -- Credential management
598
+ - [@memberjunction/actions-base](../Actions/Base/README.md) -- `RunActionParams` and `ActionResultSimple` for action definitions
599
+
600
+ Optional (for cloud key sources):
601
+ - `@aws-sdk/client-kms` -- AWS KMS integration
602
+ - `@azure/keyvault-secrets` + `@azure/identity` -- Azure Key Vault integration
492
603
 
493
604
  ## License
494
605