@memberjunction/encryption 4.0.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.
- package/README.md +347 -236
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
# @memberjunction/encryption
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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.
|
|
98
|
+
### 2. Register the Key in the Database
|
|
38
99
|
|
|
39
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
315
|
+
Uses cosmiconfig to locate configuration files in standard locations (`mj.config.cjs`, `mj.config.js`, `.mjrc.json`, `.mjrc.yaml`).
|
|
138
316
|
|
|
139
|
-
|
|
317
|
+
### AWS KMS
|
|
140
318
|
|
|
141
|
-
|
|
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.
|
|
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
|
|
365
|
+
# Then KeyLookupValue can be: mj-encryption-key
|
|
235
366
|
```
|
|
236
367
|
|
|
237
368
|
### Custom Provider
|
|
238
369
|
|
|
239
|
-
Extend `EncryptionKeySourceBase`
|
|
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
|
-
|
|
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
|
-
|
|
428
|
+
### Key Rotation
|
|
320
429
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
### Creating API Keys
|
|
430
|
+
Rotate keys without downtime using the `RotateEncryptionKeyAction`:
|
|
324
431
|
|
|
325
432
|
```typescript
|
|
326
|
-
import {
|
|
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
|
-
|
|
435
|
+
// 1. Deploy new key to environment
|
|
436
|
+
// export MJ_ENCRYPTION_KEY_PII_V2=new-base64-key-here
|
|
349
437
|
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
if (!EncryptionEngine.Instance.IsValidAPIKeyFormat(key)) {
|
|
383
|
-
throw new Error('Invalid API key format');
|
|
384
|
-
}
|
|
475
|
+
## API Reference
|
|
385
476
|
|
|
386
|
-
|
|
387
|
-
const revoked = await EncryptionEngine.Instance.RevokeAPIKey(apiKeyId, contextUser);
|
|
388
|
-
```
|
|
477
|
+
### EncryptionEngine
|
|
389
478
|
|
|
390
|
-
|
|
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
|
-
|
|
520
|
+
## Database Schema
|
|
393
521
|
|
|
394
|
-
|
|
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:
|
|
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
|
-
|
|
526
|
+
**MJ: Encryption Algorithms** -- Available algorithms (AES-256-GCM, etc.) with Node.js crypto identifiers
|
|
407
527
|
|
|
408
|
-
|
|
528
|
+
**MJ: Encryption Keys** -- Configured keys linking a source and algorithm together
|
|
409
529
|
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
415
|
-
```
|
|
416
|
-
$ENC$550e8400-e29b-41d4-a716-446655440000$AES-256-GCM$Base64IV$Base64Ciphertext$Base64AuthTag
|
|
417
|
-
```
|
|
536
|
+
## Performance
|
|
418
537
|
|
|
419
|
-
|
|
420
|
-
-
|
|
421
|
-
-
|
|
422
|
-
-
|
|
423
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
475
|
-
-
|
|
476
|
-
-
|
|
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
|
|
579
|
+
- The base64 string should be approximately 44 characters
|
|
482
580
|
|
|
483
|
-
### "Failed to decrypt"
|
|
484
|
-
- The key may have been rotated
|
|
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
|
|
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
|
-
-
|
|
491
|
-
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/encryption",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "MemberJunction: Field-level encryption engine with pluggable key sources. Server-side only - provides AES-256-GCM/CBC encryption with environment variable, config file, AWS KMS, and Azure Key Vault key sources.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"typescript": "^5.9.3"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@memberjunction/global": "4.
|
|
28
|
-
"@memberjunction/core": "4.
|
|
29
|
-
"@memberjunction/core-entities": "4.
|
|
30
|
-
"@memberjunction/credentials": "4.
|
|
31
|
-
"@memberjunction/actions-base": "4.
|
|
27
|
+
"@memberjunction/global": "4.1.0",
|
|
28
|
+
"@memberjunction/core": "4.1.0",
|
|
29
|
+
"@memberjunction/core-entities": "4.1.0",
|
|
30
|
+
"@memberjunction/credentials": "4.1.0",
|
|
31
|
+
"@memberjunction/actions-base": "4.1.0",
|
|
32
32
|
"cosmiconfig": "^9.0.0"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|