@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
package/README.md CHANGED
@@ -1,45 +1,408 @@
1
1
  # @memberjunction/encryption
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
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
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ ## Features
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
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
8
13
 
9
- ## Purpose
14
+ ## Installation
10
15
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/encryption`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
16
+ ```bash
17
+ npm install @memberjunction/encryption
18
+ ```
15
19
 
16
- ## What is OIDC Trusted Publishing?
20
+ ## Quick Start
17
21
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
22
+ ### 1. Set Up Encryption Key
19
23
 
20
- ## Setup Instructions
24
+ Create a 256-bit (32 byte) encryption key:
21
25
 
22
- To properly configure OIDC trusted publishing for this package:
26
+ ```bash
27
+ # Generate a secure key
28
+ openssl rand -base64 32
29
+ ```
23
30
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
31
+ Store it in an environment variable:
28
32
 
29
- ## DO NOT USE THIS PACKAGE
33
+ ```bash
34
+ export MJ_ENCRYPTION_KEY_PII=your-base64-key-here
35
+ ```
30
36
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
37
+ ### 2. Configure the Encryption Key in Database
36
38
 
37
- ## More Information
39
+ Run the migration to create encryption infrastructure, then register your key:
38
40
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
41
+ ```sql
42
+ -- Insert your encryption key (after running the migration)
43
+ INSERT INTO [${flyway:defaultSchema}].[EncryptionKey] (
44
+ ID, Name, Description, EncryptionKeySourceID, EncryptionAlgorithmID,
45
+ KeyLookupValue, KeyVersion, Marker, IsActive, Status, ActivatedAt
46
+ )
47
+ VALUES (
48
+ NEWID(),
49
+ 'PII Master Key',
50
+ 'Encryption key for personally identifiable information',
51
+ '38A961D2-022B-49C2-919F-1825A0E9C6F9', -- EnvVarKeySource
52
+ 'B2E88E95-D09B-4DA6-B0AE-511B21B70952', -- AES-256-GCM
53
+ 'MJ_ENCRYPTION_KEY_PII',
54
+ '1',
55
+ '$ENC$',
56
+ 1,
57
+ 'Active',
58
+ SYSDATETIMEOFFSET()
59
+ );
60
+ ```
42
61
 
43
- ---
62
+ ### 3. Enable Encryption on Entity Fields
44
63
 
45
- **Maintained for OIDC setup purposes only**
64
+ Update the EntityField metadata to enable encryption:
65
+
66
+ ```sql
67
+ UPDATE [${flyway:defaultSchema}].[EntityField]
68
+ SET Encrypt = 1,
69
+ EncryptionKeyID = 'your-key-id-here',
70
+ AllowDecryptInAPI = 0, -- Secure default: don't send plaintext to clients
71
+ SendEncryptedValue = 0 -- Secure default: send null instead of ciphertext
72
+ WHERE Entity = 'Contacts'
73
+ AND Name IN ('SSN', 'TaxID', 'BankAccountNumber');
74
+ ```
75
+
76
+ ### 4. Encrypt Existing Data
77
+
78
+ After enabling encryption on a field, run the EnableFieldEncryption action:
79
+
80
+ ```typescript
81
+ import { EnableFieldEncryptionAction } from '@memberjunction/encryption';
82
+
83
+ const action = new EnableFieldEncryptionAction();
84
+ const result = await action.Run({
85
+ Params: [
86
+ { Name: 'EntityFieldID', Value: 'field-uuid-here' },
87
+ { Name: 'BatchSize', Value: 100 }
88
+ ],
89
+ ContextUser: currentUser
90
+ });
91
+ ```
92
+
93
+ ## API Response Behavior
94
+
95
+ The encryption system provides secure-by-default API responses:
96
+
97
+ | AllowDecryptInAPI | SendEncryptedValue | API Response |
98
+ |-------------------|-------------------|--------------|
99
+ | true | N/A | Decrypted plaintext |
100
+ | false | true | Encrypted ciphertext ($ENC$...) |
101
+ | false | false | NULL (most secure, **default**) |
102
+
103
+ ## Key Source Providers
104
+
105
+ ### Environment Variable (Default)
106
+
107
+ The simplest option - store keys in environment variables:
108
+
109
+ ```bash
110
+ # Generate a 256-bit key
111
+ openssl rand -base64 32
112
+
113
+ # Set in environment
114
+ export MJ_ENCRYPTION_KEY_PII=your-base64-key-here
115
+ ```
116
+
117
+ Database configuration:
118
+ - **EncryptionKeySourceID**: `38A961D2-022B-49C2-919F-1825A0E9C6F9`
119
+ - **KeyLookupValue**: Environment variable name (e.g., `MJ_ENCRYPTION_KEY_PII`)
120
+
121
+ ### Configuration File
122
+
123
+ Store keys in `mj.config.cjs` (not recommended for production):
124
+
125
+ ```javascript
126
+ module.exports = {
127
+ encryptionKeys: {
128
+ pii_master_key: 'base64-encoded-key-here'
129
+ }
130
+ };
131
+ ```
132
+
133
+ Database configuration:
134
+ - **EncryptionKeySourceID**: `CBF9632D-EF05-42E2-82F6-5BAC79FAA565`
135
+ - **KeyLookupValue**: Key name in config (e.g., `pii_master_key`)
136
+
137
+ ### AWS KMS
138
+
139
+ Uses AWS Key Management Service with envelope encryption. Install the optional dependency:
140
+
141
+ ```bash
142
+ npm install @aws-sdk/client-kms
143
+ ```
144
+
145
+ **Setup:**
146
+
147
+ 1. Create a symmetric CMK in AWS KMS
148
+ 2. Generate a data key:
149
+ ```bash
150
+ aws kms generate-data-key \
151
+ --key-id alias/your-cmk-alias \
152
+ --key-spec AES_256 \
153
+ --query 'CiphertextBlob' \
154
+ --output text
155
+ ```
156
+ 3. Store the output (base64 CiphertextBlob) as the KeyLookupValue
157
+
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
162
+
163
+ Database configuration:
164
+ - **EncryptionKeySourceID**: `D8E4F521-3A7B-4C9E-8F12-6B5A4C3D2E1F`
165
+ - **KeyLookupValue**: Base64-encoded CiphertextBlob from GenerateDataKey
166
+
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
+ ### Azure Key Vault
184
+
185
+ Retrieves keys from Azure Key Vault secrets. Install the optional dependencies:
186
+
187
+ ```bash
188
+ npm install @azure/keyvault-secrets @azure/identity
189
+ ```
190
+
191
+ **Setup:**
192
+
193
+ 1. Create an Azure Key Vault
194
+ 2. Create a secret containing your base64-encoded key:
195
+ ```bash
196
+ # Generate key
197
+ KEY=$(openssl rand -base64 32)
198
+
199
+ # Store in Key Vault
200
+ az keyvault secret set \
201
+ --vault-name your-vault-name \
202
+ --name mj-encryption-key \
203
+ --value "$KEY"
204
+ ```
205
+
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
210
+
211
+ Database configuration:
212
+ - **EncryptionKeySourceID**: `A2B3C4D5-E6F7-8901-2345-6789ABCDEF01`
213
+ - **KeyLookupValue**: Full secret URL or secret name (if `AZURE_KEYVAULT_URL` is set)
214
+
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
+ ```bash
233
+ export AZURE_KEYVAULT_URL=https://your-vault.vault.azure.net
234
+ # Then KeyLookupValue can just be: mj-encryption-key
235
+ ```
236
+
237
+ ### Custom Provider
238
+
239
+ Extend `EncryptionKeySourceBase` for other vault services:
240
+
241
+ ```typescript
242
+ import { RegisterClass } from '@memberjunction/global';
243
+ import { EncryptionKeySourceBase } from '@memberjunction/encryption';
244
+
245
+ @RegisterClass(EncryptionKeySourceBase, 'HashiCorpVaultKeySource')
246
+ export class HashiCorpVaultKeySource extends EncryptionKeySourceBase {
247
+ get SourceName(): string { return 'HashiCorp Vault'; }
248
+
249
+ ValidateConfiguration(): boolean {
250
+ return !!process.env.VAULT_ADDR && !!process.env.VAULT_TOKEN;
251
+ }
252
+
253
+ async GetKey(lookupValue: string): Promise<Buffer> {
254
+ // Implement vault API call to retrieve secret
255
+ // Return the key as a Buffer
256
+ }
257
+
258
+ async KeyExists(lookupValue: string): Promise<boolean> {
259
+ // Check if secret exists at path
260
+ }
261
+ }
262
+ ```
263
+
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
+ ```
289
+
290
+ ## Programmatic API
291
+
292
+ ### EncryptionEngine
293
+
294
+ ```typescript
295
+ import { EncryptionEngine } from '@memberjunction/encryption';
296
+
297
+ const engine = EncryptionEngine.Instance;
298
+
299
+ // Encrypt a value
300
+ const encrypted = await engine.Encrypt(
301
+ 'sensitive-data',
302
+ encryptionKeyId,
303
+ contextUser
304
+ );
305
+
306
+ // Decrypt a value
307
+ const decrypted = await engine.Decrypt(encrypted, contextUser);
308
+
309
+ // Check if a value is encrypted
310
+ if (engine.IsEncrypted(someValue)) {
311
+ const parts = engine.ParseEncryptedValue(someValue);
312
+ console.log(`Encrypted with key: ${parts.keyId}`);
313
+ }
314
+
315
+ // Clear caches (after key rotation)
316
+ engine.ClearCaches();
317
+ ```
318
+
319
+ ## Encrypted Value Format
320
+
321
+ Encrypted values are stored as self-describing strings:
322
+
323
+ ```
324
+ $ENC$<keyId>$<algorithm>$<iv>$<ciphertext>$<authTag>
325
+ ```
326
+
327
+ Example:
328
+ ```
329
+ $ENC$550e8400-e29b-41d4-a716-446655440000$AES-256-GCM$Base64IV$Base64Ciphertext$Base64AuthTag
330
+ ```
331
+
332
+ This format allows:
333
+ - Quick detection of encrypted values
334
+ - Identification of which key was used
335
+ - Algorithm-agnostic decryption
336
+ - Future-proof key rotation
337
+
338
+ ## Security Considerations
339
+
340
+ 1. **Key Management**
341
+ - Never store keys in the database
342
+ - Use environment variables or secure vault services
343
+ - Rotate keys regularly (recommended: annually)
344
+ - Generate keys with `openssl rand -base64 32`
345
+
346
+ 2. **Authenticated Encryption**
347
+ - AES-256-GCM provides both confidentiality and integrity
348
+ - Auth tag prevents tampering with ciphertext
349
+ - Random IVs prevent pattern analysis
350
+
351
+ 3. **API Security**
352
+ - Default: encrypted fields return `null` to clients
353
+ - Explicitly enable `AllowDecryptInAPI` only when needed
354
+ - Consider using `SendEncryptedValue` for client-side decryption scenarios
355
+
356
+ 4. **Key Rotation**
357
+ - Plan for rotation before key compromise
358
+ - Test rotation in staging environment first
359
+ - Monitor rotation progress for large datasets
360
+ - Keep old keys accessible until rotation completes
361
+
362
+ ## Database Schema
363
+
364
+ The encryption infrastructure includes three new tables:
365
+
366
+ - **MJ: Encryption Key Sources** - Where keys come from (env vars, config, vaults)
367
+ - **MJ: Encryption Algorithms** - Available algorithms (AES-256-GCM, etc.)
368
+ - **MJ: Encryption Keys** - Configured keys linking sources and algorithms
369
+
370
+ EntityField extensions:
371
+ - **Encrypt** - Enable encryption for this field
372
+ - **EncryptionKeyID** - Which key to use
373
+ - **AllowDecryptInAPI** - Whether to decrypt in API responses
374
+ - **SendEncryptedValue** - Send ciphertext when decryption not allowed
375
+
376
+ ## Performance
377
+
378
+ - Key configurations are cached with 5-minute TTL
379
+ - Key material is cached with 5-minute TTL
380
+ - Encryption/decryption uses Node.js native crypto (fast)
381
+ - Batch processing for key rotation and initial encryption
382
+ - Lazy loading - encryption engine only activated when needed
383
+
384
+ ## Troubleshooting
385
+
386
+ ### "Encryption key not found"
387
+ - Check that the key exists in `MJ: Encryption Keys` table
388
+ - Verify `IsActive = 1` and `Status = 'Active'`
389
+ - Check that the referenced algorithm and source are also active
390
+
391
+ ### "Key length mismatch"
392
+ - Ensure your key is exactly 32 bytes (256 bits) for AES-256
393
+ - Generate with: `openssl rand -base64 32`
394
+ - The base64 string should be ~44 characters
395
+
396
+ ### "Failed to decrypt"
397
+ - The key may have been rotated - check KeyVersion
398
+ - The data may be corrupted
399
+ - Auth tag mismatch indicates tampering
400
+
401
+ ### API returns null for encrypted fields
402
+ - Check `AllowDecryptInAPI` flag on the EntityField
403
+ - Default is `false` for security
404
+ - Update to `true` if API clients need plaintext
405
+
406
+ ## License
407
+
408
+ ISC