@growy/strapi-plugin-encrypted-field 2.3.2 → 2.4.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/CHANGELOG.md +96 -0
- package/README.md +179 -112
- package/package.json +4 -4
- package/scripts/rotate-key.js +117 -0
- package/server/bootstrap.js +1 -1
- package/server/middlewares/decrypt.js +1 -1
- package/server/utils/crypto.js +11 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [2.4.0] - 2026-02-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Key rotation script (`scripts/rotate-key.js`) for re-encrypting data when changing the encryption key.
|
|
9
|
+
- CHANGELOG.md with full version history.
|
|
10
|
+
- Documented `uniqueField` limitation in README (unique constraints don't work on encrypted fields due to random IV).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- All server-side error messages and logs translated to English for global developer audience.
|
|
14
|
+
- `description` fields in `package.json` translated to English.
|
|
15
|
+
- `repository.url` fixed via `npm pkg fix` (normalized to `git+https://` format).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `strapi.config.get()` path updated from deprecated `plugin.encrypted-field` to Strapi v5 convention `plugin::encrypted-field`.
|
|
19
|
+
|
|
20
|
+
## [2.3.3] - 2026-02-26
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- README rewritten in English for global npm community adoption.
|
|
24
|
+
- Removed unused `admin/src/pages/` directory.
|
|
25
|
+
|
|
26
|
+
## [2.3.2] - 2026-02-26
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Restored detailed technical documentation in README (API examples, key management warnings, regex validation guide).
|
|
30
|
+
|
|
31
|
+
## [2.3.1] - 2026-02-26
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- README converted to bilingual format (EN/ES).
|
|
35
|
+
- Removed unused server directories (`content-types`, `controllers`, `routes`, `services`).
|
|
36
|
+
- Added `.npmignore` for cleaner npm package.
|
|
37
|
+
|
|
38
|
+
## [2.3.0] - 2026-02-26
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- Multi-language support (i18n): English and Spanish translations for admin UI.
|
|
42
|
+
- Encryption key caching in memory for improved performance.
|
|
43
|
+
- `inputSize` configuration for resizable inputs in Content-Type Builder.
|
|
44
|
+
- Plugin `config` block with `default` and `validator` (Strapi v5 standard).
|
|
45
|
+
- `@strapi/design-system` and `react` as `peerDependencies`.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Refactored lifecycle hooks (`bootstrap.js`): extracted shared `processEncryption` and `processDecryption` functions to eliminate code duplication.
|
|
49
|
+
- Simplified `registerTrads` to native Strapi v5 async/await pattern.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- Decrypt middleware now resolves the root entity `modelUid` from the API request path, fixing a bug where top-level encrypted fields were not decrypted in API responses.
|
|
53
|
+
|
|
54
|
+
## [2.2.1] - 2025-10-14
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- Visibility toggle (show/hide) and copy-to-clipboard controls with confirmation notifications in admin UI.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- Updated README with improved documentation and package metadata.
|
|
61
|
+
|
|
62
|
+
## [2.0.4] - 2025-10-13
|
|
63
|
+
|
|
64
|
+
### Removed
|
|
65
|
+
- Placeholder field option removed after multiple iterations.
|
|
66
|
+
|
|
67
|
+
## [2.0.3] - 2025-10-13
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- Set predefined placeholder and removed customization option.
|
|
71
|
+
|
|
72
|
+
## [2.0.1] - 2025-10-13
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
- Nested component support for encryption/decryption at any depth.
|
|
76
|
+
- Decrypt middleware for API responses.
|
|
77
|
+
- Regex and length validation before encryption.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
- Simplified encrypted field UI.
|
|
81
|
+
- Improved props handling in Input component.
|
|
82
|
+
- Field type changed to `string`.
|
|
83
|
+
- Removed unnecessary code comments.
|
|
84
|
+
|
|
85
|
+
## [1.0.1] - 2025-10-13
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
- Package exports configuration for Strapi v5.
|
|
89
|
+
|
|
90
|
+
## [1.0.0] - 2025-10-13
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
- Initial release.
|
|
94
|
+
- Custom field "Encrypted Text" for Content-Type Builder.
|
|
95
|
+
- AES-256-GCM encryption/decryption.
|
|
96
|
+
- Basic admin panel input component.
|
package/README.md
CHANGED
|
@@ -6,28 +6,22 @@
|
|
|
6
6
|
<img src="https://img.shields.io/badge/Strapi-v5-blueviolet" alt="Strapi v5" />
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- ✅ **
|
|
22
|
-
- ✅ **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- ✅ **Native Strapi v5 UI** with visibility controls, redimensionable inputs and copy to clipboard.
|
|
26
|
-
- ✅ **Multi-language support (i18n)**: English and Spanish.
|
|
27
|
-
- ✅ **Encrypted Data** in database with unique IV and Auth Tag.
|
|
28
|
-
- ✅ **Nested Components support** at any depth.
|
|
29
|
-
|
|
30
|
-
### Installation
|
|
9
|
+
Official **Growy AI** plugin for Strapi that provides a custom encrypted text field using AES-256-GCM. Protect sensitive information directly in your database with transparent encryption and robust validation.
|
|
10
|
+
|
|
11
|
+
- ✅ **Custom field** "Encrypted Text" in the Content-Type Builder
|
|
12
|
+
- ✅ **Automatic encryption** AES-256-GCM on save
|
|
13
|
+
- ✅ **Transparent decryption** on read (admin panel and API)
|
|
14
|
+
- ✅ **Backend validation** with regex and length constraint support
|
|
15
|
+
- ✅ **Native Strapi v5 UI** with visibility controls, resizable inputs and copy to clipboard
|
|
16
|
+
- ✅ **Values hidden** by default with show/hide toggle
|
|
17
|
+
- ✅ **Copy notifications** confirmation when copying values
|
|
18
|
+
- ✅ **Multi-language support (i18n)**: English and Spanish
|
|
19
|
+
- ✅ **Robust key management** with validation and clear error messages
|
|
20
|
+
- ✅ **Encrypted data** in database with unique IV and Auth Tag per operation
|
|
21
|
+
- ✅ **Reusable** in any collection or component
|
|
22
|
+
- ✅ **Full support** for nested components and complex structures
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
31
25
|
|
|
32
26
|
```bash
|
|
33
27
|
npm install @growy/strapi-plugin-encrypted-field
|
|
@@ -35,10 +29,12 @@ npm install @growy/strapi-plugin-encrypted-field
|
|
|
35
29
|
yarn add @growy/strapi-plugin-encrypted-field
|
|
36
30
|
```
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
### 1. Enable the plugin
|
|
35
|
+
|
|
36
|
+
Create or edit `config/plugins.js` or `config/plugins.ts`:
|
|
39
37
|
|
|
40
|
-
#### 1. Enable the plugin
|
|
41
|
-
Edit `config/plugins.js` or `config/plugins.ts`:
|
|
42
38
|
```javascript
|
|
43
39
|
module.exports = {
|
|
44
40
|
'encrypted-field': {
|
|
@@ -47,140 +43,211 @@ module.exports = {
|
|
|
47
43
|
};
|
|
48
44
|
```
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
### 2. Configure the encryption key (REQUIRED)
|
|
47
|
+
|
|
48
|
+
#### Option A: Environment variable (recommended)
|
|
49
|
+
|
|
51
50
|
Add to your `.env`:
|
|
51
|
+
|
|
52
52
|
```bash
|
|
53
53
|
ENCRYPTION_KEY=your_64_character_hex_key_here
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
#### Option B: Configuration file
|
|
57
|
+
|
|
58
|
+
Edit `config/plugins.js`:
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
module.exports = ({ env }) => ({
|
|
62
|
+
'encrypted-field': {
|
|
63
|
+
enabled: true,
|
|
64
|
+
config: {
|
|
65
|
+
encryptionKey: env('ENCRYPTION_KEY'),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Generate a secure key
|
|
72
|
+
|
|
57
73
|
```bash
|
|
58
74
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
59
75
|
```
|
|
60
76
|
|
|
77
|
+
This will generate a 64-character hexadecimal key (32 bytes).
|
|
78
|
+
|
|
61
79
|
⚠️ **CRITICAL - Key Management**:
|
|
62
|
-
- **Store the key
|
|
63
|
-
- **Never** include it in version control
|
|
64
|
-
- **If you lose the key**, you will
|
|
65
|
-
- **Use the same key** across all environments sharing the same database
|
|
80
|
+
- **Store the key securely** (secrets manager, encrypted environment variables)
|
|
81
|
+
- **Never** include it in version control
|
|
82
|
+
- **If you lose the key**, you will not be able to decrypt existing data
|
|
83
|
+
- **Use the same key** across all environments sharing the same database
|
|
84
|
+
- **For production**, consider services like AWS Secrets Manager, HashiCorp Vault or similar
|
|
66
85
|
|
|
67
|
-
###
|
|
86
|
+
### 3. Rebuild the admin
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run build
|
|
90
|
+
npm run develop
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Requirements
|
|
94
|
+
|
|
95
|
+
- **Strapi**: v5.0.0 or higher
|
|
96
|
+
- **Node.js**: 18.x - 22.x
|
|
97
|
+
- **npm**: 6.0.0 or higher
|
|
98
|
+
|
|
99
|
+
## Data Validation
|
|
68
100
|
|
|
69
|
-
#### Data Validation
|
|
70
101
|
The plugin supports validation before encryption:
|
|
71
|
-
1. In Content-Type Builder, select the encrypted field.
|
|
72
|
-
2. Go to **"Advanced Settings"**.
|
|
73
|
-
3. In **"RegEx pattern"**, enter your regular expression.
|
|
74
|
-
**Example**: To validate an API key format: `^sk-[a-zA-Z0-9]{32}$`.
|
|
75
102
|
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
### Configure regex validation
|
|
104
|
+
|
|
105
|
+
1. In Content-Type Builder, select the encrypted field
|
|
106
|
+
2. Go to the **"Advanced Settings"** tab
|
|
107
|
+
3. In **"RegEx pattern"**, enter your regular expression
|
|
108
|
+
4. Save the changes
|
|
109
|
+
|
|
110
|
+
**Example**: To validate API key format:
|
|
111
|
+
```regex
|
|
112
|
+
^sk-[a-zA-Z0-9]{32}$
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the value does not match the pattern, an error will be thrown before encryption.
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
### 1. Add an encrypted field to a collection
|
|
120
|
+
|
|
121
|
+
1. Go to **Content-Type Builder**
|
|
122
|
+
2. Select a collection or create a new one
|
|
123
|
+
3. Click **"Add another field"**
|
|
124
|
+
4. Search for **"Encrypted Text"** (with 🔒 icon)
|
|
125
|
+
5. Set the field name
|
|
126
|
+
6. Save and restart Strapi
|
|
127
|
+
|
|
128
|
+
### 2. Using the field
|
|
129
|
+
|
|
130
|
+
The field works like a regular text field with additional security features:
|
|
131
|
+
|
|
132
|
+
- **In the panel**: Type text normally
|
|
133
|
+
- **Hidden values**: Values are shown as `***` by default
|
|
134
|
+
- **Eye button**: Toggles between show/hide the value
|
|
135
|
+
- **Copy button**: Copies the value to clipboard with a confirmation notification
|
|
136
|
+
- **On save**: Automatically encrypted
|
|
137
|
+
- **On read**: Automatically decrypted
|
|
138
|
+
- **In the DB**: Stored encrypted with format `iv:authTag:encrypted`
|
|
139
|
+
- **In components**: Works the same in nested components at any depth
|
|
140
|
+
|
|
141
|
+
### 3. API Usage
|
|
142
|
+
|
|
78
143
|
```bash
|
|
79
|
-
# Create an
|
|
144
|
+
# Create with an encrypted field
|
|
80
145
|
curl -X POST http://localhost:1337/api/users \
|
|
81
146
|
-H "Content-Type: application/json" \
|
|
82
|
-
-d '{
|
|
147
|
+
-d '{
|
|
148
|
+
"data": {
|
|
149
|
+
"name": "John",
|
|
150
|
+
"apiKey": "my-secret-key-123"
|
|
151
|
+
}
|
|
152
|
+
}'
|
|
83
153
|
|
|
84
154
|
# Read (returns decrypted)
|
|
85
155
|
curl -X GET http://localhost:1337/api/users/1
|
|
86
|
-
# Response: { "
|
|
156
|
+
# Response: { "name": "John", "apiKey": "my-secret-key-123" }
|
|
87
157
|
```
|
|
88
158
|
|
|
89
|
-
|
|
159
|
+
## Usage Example
|
|
90
160
|
|
|
91
|
-
|
|
161
|
+
### "User" collection with an encrypted API Key
|
|
92
162
|
|
|
93
|
-
|
|
163
|
+
**Schema:**
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"name": "string",
|
|
167
|
+
"email": "email",
|
|
168
|
+
"apiKey": "plugin::encrypted-field.encrypted-text"
|
|
169
|
+
}
|
|
170
|
+
```
|
|
94
171
|
|
|
95
|
-
|
|
172
|
+
**In the DB:**
|
|
173
|
+
```
|
|
174
|
+
apiKey: "a1b2c3d4e5f6....:f9e8d7c6b5a4....:9f8e7d6c5b4a3..."
|
|
175
|
+
```
|
|
96
176
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
- ✅ **UI Nativa Strapi v5** con controles de visibilidad, inputs redimensionables y copiar al portapapeles.
|
|
102
|
-
- ✅ **Soporte multi-idioma (i18n)**: Inglés y Español.
|
|
103
|
-
- ✅ **Datos cifrados** en base de datos con IV único y Auth Tag.
|
|
104
|
-
- ✅ **Soporte para componentes anidados** a cualquier profundidad.
|
|
177
|
+
**In the panel and API:**
|
|
178
|
+
```
|
|
179
|
+
apiKey: "sk-1234567890abcdef"
|
|
180
|
+
```
|
|
105
181
|
|
|
106
|
-
|
|
182
|
+
## Security & Architecture
|
|
107
183
|
|
|
108
|
-
|
|
109
|
-
npm install @growy/strapi-plugin-encrypted-field
|
|
110
|
-
# o
|
|
111
|
-
yarn add @growy/strapi-plugin-encrypted-field
|
|
112
|
-
```
|
|
184
|
+
### Technical Specifications
|
|
113
185
|
|
|
114
|
-
|
|
186
|
+
- **Algorithm**: AES-256-GCM (NIST standard, military grade)
|
|
187
|
+
- **Key size**: 256 bits (32 bytes, 64 hex characters)
|
|
188
|
+
- **IV (Initialization Vector)**: 96 bits (12 bytes) randomly generated per operation
|
|
189
|
+
- **Auth Tag**: 128 bits (16 bytes) for integrity verification
|
|
190
|
+
- **Stored format**: `iv:authTag:encryptedData` (all in hexadecimal)
|
|
191
|
+
- **Key caching**: Encryption key is parsed and cached in memory for optimal performance
|
|
115
192
|
|
|
116
|
-
|
|
117
|
-
Edita `config/plugins.js`:
|
|
118
|
-
```javascript
|
|
119
|
-
module.exports = {
|
|
120
|
-
'encrypted-field': {
|
|
121
|
-
enabled: true,
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
```
|
|
193
|
+
### Security Features
|
|
125
194
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
195
|
+
- ✅ **Authenticated encryption**: GCM provides both confidentiality and integrity
|
|
196
|
+
- ✅ **Unique IV**: Every encryption operation generates a random IV
|
|
197
|
+
- ✅ **Tamper resistance**: Auth Tag detects any modification
|
|
198
|
+
- ✅ **Input validation**: Regex and custom constraints supported
|
|
199
|
+
- ✅ **Safe error handling**: Controlled logs without exposing sensitive data
|
|
200
|
+
- ✅ **Double-layer decryption**: Lifecycle hooks (internal) + middleware (API responses)
|
|
131
201
|
|
|
132
|
-
|
|
133
|
-
- **Guarda la clave de forma segura** (gestor de secretos, variables de entorno cifradas).
|
|
134
|
-
- **Nunca** la incluyas en el control de versiones.
|
|
135
|
-
- **Si pierdes la clave**, NO podrás descifrar los datos existentes.
|
|
202
|
+
### Best Practices
|
|
136
203
|
|
|
137
|
-
|
|
204
|
+
1. **Key rotation**: Use the included rotation script (see below)
|
|
205
|
+
2. **Environment separation**: Use different keys per dev/staging/prod
|
|
206
|
+
3. **Auditing**: Monitor encryption/decryption error logs
|
|
207
|
+
4. **Key backup**: Keep secure copies of keys in multiple locations
|
|
208
|
+
5. **Private fields**: Mark sensitive fields as "private" to exclude them from the public API
|
|
138
209
|
|
|
139
|
-
|
|
140
|
-
El plugin soporta validación antes del cifrado:
|
|
141
|
-
1. En el Content-Type Builder, selecciona el campo cifrado.
|
|
142
|
-
2. Ve a la pestaña **"Advanced Settings"**.
|
|
143
|
-
3. En **"RegEx pattern"**, ingresa tu expresión regular.
|
|
144
|
-
**Ejemplo**: Para validar formato de API key: `^sk-[a-zA-Z0-9]{32}$`.
|
|
210
|
+
### Key Rotation
|
|
145
211
|
|
|
146
|
-
|
|
147
|
-
La API devuelve los valores descifrados automáticamente.
|
|
148
|
-
```bash
|
|
149
|
-
# Crear con campo cifrado
|
|
150
|
-
curl -X POST http://localhost:1337/api/usuarios \
|
|
151
|
-
-H "Content-Type: application/json" \
|
|
152
|
-
-d '{"data": {"apiKey": "mi-clave-secreta-123"}}'
|
|
212
|
+
If you need to change your encryption key, use the included rotation script to re-encrypt existing data:
|
|
153
213
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Response: { "apiKey": "mi-clave-secreta-123" }
|
|
214
|
+
```bash
|
|
215
|
+
node scripts/rotate-key.js --old=<CURRENT_64_CHAR_KEY> --new=<NEW_64_CHAR_KEY>
|
|
157
216
|
```
|
|
158
217
|
|
|
159
|
-
|
|
218
|
+
The script reads encrypted values from stdin, decrypts with the old key, and re-encrypts with the new key. See the script output for database-specific integration examples (PostgreSQL, etc.).
|
|
160
219
|
|
|
161
|
-
|
|
162
|
-
- **IV (Initialization Vector)**: 96 bits generado aleatoriamente por operación.
|
|
163
|
-
- **Integridad**: Auth Tag de 128 bits para detectar manipulaciones.
|
|
164
|
-
- **Formato almacenado**: `iv:authTag:encryptedData`.
|
|
220
|
+
## Use Cases
|
|
165
221
|
|
|
166
|
-
|
|
222
|
+
- 🔑 Third-party API Keys
|
|
223
|
+
- 🔐 Access tokens
|
|
224
|
+
- 🔒 Webhook secrets
|
|
225
|
+
- 💳 Sensitive information
|
|
226
|
+
- 📧 SMTP credentials
|
|
227
|
+
- 🔑 Application passwords
|
|
167
228
|
|
|
168
|
-
|
|
169
|
-
- ❌ **Ordenamiento**: No se puede ordenar por campos cifrados.
|
|
170
|
-
- ❌ **Filtros**: No se pueden aplicar filtros directos en la consulta a la BD.
|
|
229
|
+
## Known Limitations
|
|
171
230
|
|
|
172
|
-
|
|
231
|
+
- ❌ **Search**: Cannot search by encrypted fields (data is encrypted in DB)
|
|
232
|
+
- ❌ **Sorting**: Cannot sort by encrypted fields
|
|
233
|
+
- ❌ **Filters**: Cannot apply direct filters on encrypted fields
|
|
234
|
+
- ❌ **Unique constraint**: Strapi's unique validation will not work correctly on encrypted fields because each encryption produces a different ciphertext (random IV)
|
|
235
|
+
- ⚠️ **Performance**: Encryption/decryption adds minimal overhead (~1-2ms per operation)
|
|
236
|
+
- ⚠️ **Key synchronization**: All environments sharing the same DB must use the same key
|
|
237
|
+
|
|
238
|
+
## License
|
|
173
239
|
|
|
174
|
-
## License / Licencia
|
|
175
240
|
MIT © 2025 Growy AI
|
|
176
241
|
|
|
177
|
-
##
|
|
178
|
-
|
|
179
|
-
**
|
|
242
|
+
## Developed by
|
|
243
|
+
|
|
244
|
+
**Growy AI** - AI and business automation solutions
|
|
245
|
+
|
|
246
|
+
**Main author**: Zahir El isaac
|
|
180
247
|
|
|
181
248
|
---
|
|
182
249
|
|
|
183
250
|
<div align="center">
|
|
184
|
-
<p>If this plugin is useful to you, consider giving it a ⭐ on GitHub
|
|
185
|
-
<p>Made with ❤️ by Growy AI
|
|
251
|
+
<p>If this plugin is useful to you, consider giving it a ⭐ on GitHub</p>
|
|
252
|
+
<p>Made with ❤️ by the Growy AI team</p>
|
|
186
253
|
</div>
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growy/strapi-plugin-encrypted-field",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Custom encrypted text field plugin for Strapi using AES-256-GCM",
|
|
5
5
|
"strapi": {
|
|
6
6
|
"name": "encrypted-field",
|
|
7
7
|
"displayName": "Encrypted Field",
|
|
8
|
-
"description": "
|
|
8
|
+
"description": "Adds an AES-256-GCM encrypted text field",
|
|
9
9
|
"kind": "plugin"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {},
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/ZahirElIsaac/strapi-campo-encriptado.git"
|
|
30
|
+
"url": "git+https://github.com/ZahirElIsaac/strapi-campo-encriptado.git"
|
|
31
31
|
},
|
|
32
32
|
"contributors": [
|
|
33
33
|
{
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
7
|
+
const IV_LENGTH = 12;
|
|
8
|
+
const AUTH_TAG_LENGTH = 16;
|
|
9
|
+
|
|
10
|
+
function parseKey(hexKey) {
|
|
11
|
+
if (!hexKey || typeof hexKey !== 'string' || hexKey.length !== 64) {
|
|
12
|
+
throw new Error(`Key must be exactly 64 hexadecimal characters. Got: ${hexKey?.length || 0}`);
|
|
13
|
+
}
|
|
14
|
+
if (!/^[0-9a-fA-F]{64}$/.test(hexKey)) {
|
|
15
|
+
throw new Error('Key must contain only hexadecimal characters (0-9, a-f, A-F)');
|
|
16
|
+
}
|
|
17
|
+
return Buffer.from(hexKey, 'hex');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decryptWithKey(encryptedText, keyBuffer) {
|
|
21
|
+
const parts = encryptedText.split(':');
|
|
22
|
+
if (parts.length !== 3) return null;
|
|
23
|
+
|
|
24
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
25
|
+
if (ivHex.length !== IV_LENGTH * 2 || authTagHex.length !== AUTH_TAG_LENGTH * 2) return null;
|
|
26
|
+
|
|
27
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
28
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
29
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
|
|
30
|
+
decipher.setAuthTag(authTag);
|
|
31
|
+
|
|
32
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
33
|
+
decrypted += decipher.final('utf8');
|
|
34
|
+
return decrypted;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function encryptWithKey(text, keyBuffer) {
|
|
38
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
39
|
+
const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv);
|
|
40
|
+
|
|
41
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
42
|
+
encrypted += cipher.final('hex');
|
|
43
|
+
const authTag = cipher.getAuthTag();
|
|
44
|
+
|
|
45
|
+
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function main() {
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const oldKeyArg = args.find((a) => a.startsWith('--old='));
|
|
51
|
+
const newKeyArg = args.find((a) => a.startsWith('--new='));
|
|
52
|
+
|
|
53
|
+
if (!oldKeyArg || !newKeyArg) {
|
|
54
|
+
console.error('Usage: node scripts/rotate-key.js --old=<OLD_KEY> --new=<NEW_KEY>');
|
|
55
|
+
console.error('Keys must be 64-character hexadecimal strings.');
|
|
56
|
+
console.error('\nGenerate a new key with:');
|
|
57
|
+
console.error(' node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const oldKey = oldKeyArg.split('=')[1];
|
|
62
|
+
const newKey = newKeyArg.split('=')[1];
|
|
63
|
+
|
|
64
|
+
let oldKeyBuffer, newKeyBuffer;
|
|
65
|
+
try {
|
|
66
|
+
oldKeyBuffer = parseKey(oldKey);
|
|
67
|
+
newKeyBuffer = parseKey(newKey);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`Key validation error: ${error.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (oldKey === newKey) {
|
|
74
|
+
console.error('Old and new keys are identical. Aborting.');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
79
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
80
|
+
|
|
81
|
+
console.log('\n🔑 Encrypted Field - Key Rotation Tool');
|
|
82
|
+
console.log('━'.repeat(45));
|
|
83
|
+
console.log('\nThis script reads encrypted values from stdin (one per line),');
|
|
84
|
+
console.log('decrypts them with the OLD key, and re-encrypts with the NEW key.');
|
|
85
|
+
console.log('\nTo use with a database, export the encrypted column values,');
|
|
86
|
+
console.log('pipe them through this script, and update the database.\n');
|
|
87
|
+
console.log('Example with PostgreSQL:');
|
|
88
|
+
console.log(' psql -t -A -c "SELECT id, api_key FROM users WHERE api_key IS NOT NULL" |\\');
|
|
89
|
+
console.log(' while IFS="|" read id val; do');
|
|
90
|
+
console.log(' newval=$(echo "$val" | node scripts/rotate-key.js --old=X --new=Y)');
|
|
91
|
+
console.log(' psql -c "UPDATE users SET api_key=\'$newval\' WHERE id=$id"');
|
|
92
|
+
console.log(' done\n');
|
|
93
|
+
console.log('Paste encrypted values (one per line). Press Ctrl+D when done:\n');
|
|
94
|
+
|
|
95
|
+
rl.on('line', (line) => {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const decrypted = decryptWithKey(trimmed, oldKeyBuffer);
|
|
101
|
+
if (decrypted === null) {
|
|
102
|
+
console.error(`SKIP (not encrypted format): ${trimmed.substring(0, 30)}...`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const reEncrypted = encryptWithKey(decrypted, newKeyBuffer);
|
|
106
|
+
console.log(reEncrypted);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`ERROR: ${error.message} | Input: ${trimmed.substring(0, 30)}...`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
rl.on('close', () => {
|
|
113
|
+
console.error('\n✅ Key rotation complete.');
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main();
|
package/server/bootstrap.js
CHANGED
|
@@ -17,7 +17,7 @@ function processEncryption(event, strapi) {
|
|
|
17
17
|
|
|
18
18
|
const validation = validateValue(value, attribute);
|
|
19
19
|
if (!validation.valid) {
|
|
20
|
-
throw new Error(`
|
|
20
|
+
throw new Error(`Validation failed for field "${key}": ${validation.error}`);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
data[key] = encrypt(value, strapi);
|
|
@@ -50,7 +50,7 @@ module.exports = (config, { strapi }) => {
|
|
|
50
50
|
try {
|
|
51
51
|
obj[key] = decrypt(obj[key], strapi);
|
|
52
52
|
} catch (error) {
|
|
53
|
-
strapi.log.error(`
|
|
53
|
+
strapi.log.error(`Decryption error on field ${key}: ${error.message}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
package/server/utils/crypto.js
CHANGED
|
@@ -9,10 +9,10 @@ let _cachedKey = null;
|
|
|
9
9
|
let _cachedKeySource = null;
|
|
10
10
|
|
|
11
11
|
function getEncryptionKey(strapi) {
|
|
12
|
-
const key = process.env.ENCRYPTION_KEY || strapi?.config?.get('plugin
|
|
12
|
+
const key = process.env.ENCRYPTION_KEY || strapi?.config?.get('plugin::encrypted-field.encryptionKey');
|
|
13
13
|
|
|
14
14
|
if (!key) {
|
|
15
|
-
const errorMsg = '⚠️ ENCRYPTION_KEY
|
|
15
|
+
const errorMsg = '⚠️ ENCRYPTION_KEY not configured. You must set a 64-character hexadecimal key in environment variables or Strapi configuration.';
|
|
16
16
|
if (strapi?.log?.error) {
|
|
17
17
|
strapi.log.error(errorMsg);
|
|
18
18
|
}
|
|
@@ -24,11 +24,11 @@ function getEncryptionKey(strapi) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (typeof key !== 'string' || key.length !== 64) {
|
|
27
|
-
throw new Error(`ENCRYPTION_KEY
|
|
27
|
+
throw new Error(`ENCRYPTION_KEY must be exactly 64 hexadecimal characters (32 bytes). Current: ${key?.length || 0}`);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if (!/^[0-9a-fA-F]{64}$/.test(key)) {
|
|
31
|
-
throw new Error('ENCRYPTION_KEY
|
|
31
|
+
throw new Error('ENCRYPTION_KEY must contain only hexadecimal characters (0-9, a-f, A-F)');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
_cachedKey = Buffer.from(key, 'hex');
|
|
@@ -54,7 +54,7 @@ function encrypt(text, strapi) {
|
|
|
54
54
|
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
55
55
|
} catch (error) {
|
|
56
56
|
if (strapi?.log?.error) {
|
|
57
|
-
strapi.log.error(`
|
|
57
|
+
strapi.log.error(`Encryption error: ${error.message}`);
|
|
58
58
|
}
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
@@ -88,7 +88,7 @@ function decrypt(encryptedText, strapi) {
|
|
|
88
88
|
return decrypted;
|
|
89
89
|
} catch (error) {
|
|
90
90
|
if (strapi?.log?.debug) {
|
|
91
|
-
strapi.log.debug(`
|
|
91
|
+
strapi.log.debug(`Decryption error: ${error.message}. Returning original text.`);
|
|
92
92
|
}
|
|
93
93
|
return encryptedText;
|
|
94
94
|
}
|
|
@@ -102,7 +102,7 @@ function validateValue(value, attribute) {
|
|
|
102
102
|
if (typeof value !== 'string') {
|
|
103
103
|
return {
|
|
104
104
|
valid: false,
|
|
105
|
-
error: '
|
|
105
|
+
error: 'Value must be a string'
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -112,13 +112,13 @@ function validateValue(value, attribute) {
|
|
|
112
112
|
if (!regex.test(value)) {
|
|
113
113
|
return {
|
|
114
114
|
valid: false,
|
|
115
|
-
error: `
|
|
115
|
+
error: `Value does not match the validation pattern: ${attribute.regex}`
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
} catch (error) {
|
|
119
119
|
return {
|
|
120
120
|
valid: false,
|
|
121
|
-
error: `
|
|
121
|
+
error: `Invalid regex pattern: ${error.message}`
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -126,14 +126,14 @@ function validateValue(value, attribute) {
|
|
|
126
126
|
if (attribute.maxLength && value.length > attribute.maxLength) {
|
|
127
127
|
return {
|
|
128
128
|
valid: false,
|
|
129
|
-
error: `
|
|
129
|
+
error: `Value exceeds maximum length of ${attribute.maxLength} characters`
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
if (attribute.minLength && value.length < attribute.minLength) {
|
|
134
134
|
return {
|
|
135
135
|
valid: false,
|
|
136
|
-
error: `
|
|
136
|
+
error: `Value must be at least ${attribute.minLength} characters`
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
139
|
|