@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 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
- ### [English](#english-version) | [Español](#versión-en-español)
12
-
13
- ---
14
-
15
- ## English Version
16
-
17
- Official **Growy AI** plugin for Strapi that provide a custom encrypted text field using AES-256-GCM. Protect sensitive information directly in your database with transparent encryption and robust validation.
18
-
19
- ### Features
20
-
21
- - ✅ **Custom Field** "Encrypted Text" in the Content-Type Builder.
22
- - ✅ **Automatic Encryption** AES-256-GCM when saving.
23
- - ✅ **Transparent Decryption** when reading (Admin panel and API).
24
- - ✅ **Backend Validation** with regex support and length constraints.
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
- ### Configuration
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
- #### 2. Configure Encryption Key (REQUIRED)
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
- **Generate a secure key:**
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 safely** (Secrets manager, encrypted env vars).
63
- - **Never** include it in version control.
64
- - **If you lose the key**, you will NOT be able to decrypt existing data.
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
- ### Usage & Validation
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
- #### API Usage
77
- The API returns decrypted values automatically for authorized requests.
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 entry
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 '{"data": {"apiKey": "my-secret-token"}}'
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: { "data": { "apiKey": "my-secret-token" } }
156
+ # Response: { "name": "John", "apiKey": "my-secret-key-123" }
87
157
  ```
88
158
 
89
- ---
159
+ ## Usage Example
90
160
 
91
- ## Versión en Español
161
+ ### "User" collection with an encrypted API Key
92
162
 
93
- Plugin oficial de **Growy AI** para Strapi que proporciona un campo personalizado de texto cifrado con AES-256-GCM. Protege información sensible directamente en tu base de datos con cifrado transparente y validación robusta.
163
+ **Schema:**
164
+ ```json
165
+ {
166
+ "name": "string",
167
+ "email": "email",
168
+ "apiKey": "plugin::encrypted-field.encrypted-text"
169
+ }
170
+ ```
94
171
 
95
- ### Características
172
+ **In the DB:**
173
+ ```
174
+ apiKey: "a1b2c3d4e5f6....:f9e8d7c6b5a4....:9f8e7d6c5b4a3..."
175
+ ```
96
176
 
97
- - ✅ **Campo personalizado** "Texto Cifrado" en el Content-Type Builder.
98
- - ✅ **Cifrado automático** AES-256-GCM al guardar.
99
- - ✅ **Descifrado transparente** al leer (panel y API).
100
- - ✅ **Validación backend** con soporte para regex y restricciones.
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
- ### Instalación
182
+ ## Security & Architecture
107
183
 
108
- ```bash
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
- ### Configuración
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
- #### 1. Habilitar el plugin
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
- #### 2. Configurar la clave (REQUERIDO)
127
- Agrega a tu `.env`:
128
- ```bash
129
- ENCRYPTION_KEY=tu_clave_de_64_caracteres_hexadecimales_aqui
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
- ⚠️ **CRÍTICO - Gestión de claves**:
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
- ### Uso y Validación
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
- #### Validación de datos
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
- #### Uso por API
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
- # Leer (devuelve descifrado)
155
- curl -X GET http://localhost:1337/api/usuarios/1
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
- ### Especificaciones Técnicas
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
- - **Algoritmo**: AES-256-GCM (Grado militar).
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
- ### Limitaciones Conocidas
222
+ - 🔑 Third-party API Keys
223
+ - 🔐 Access tokens
224
+ - 🔒 Webhook secrets
225
+ - 💳 Sensitive information
226
+ - 📧 SMTP credentials
227
+ - 🔑 Application passwords
167
228
 
168
- - **Búsqueda**: No se puede buscar por campos cifrados debido al cifrado en BD.
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
- ## Credits / Créditos
178
- **Growy AI** - Soluciones de IA y automatización empresarial
179
- **Main Author / Autor principal**: Zahir El isaac
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 / Si este plugin te resulta útil, considera darle una ⭐ en GitHub</p>
185
- <p>Made with ❤️ by Growy AI Team</p>
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.3.2",
4
- "description": "Campo personalizado de texto cifrado para Strapi",
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": "Agrega un campo de texto cifrado con AES-256-GCM",
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();
@@ -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(`Validación fallida para el campo "${key}": ${validation.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(`Error descifrando campo ${key}: ${error.message}`);
53
+ strapi.log.error(`Decryption error on field ${key}: ${error.message}`);
54
54
  }
55
55
  }
56
56
  }
@@ -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.encrypted-field.encryptionKey');
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 no configurada. Debe establecer una clave de 64 caracteres hexadecimales en las variables de entorno o configuración de Strapi.';
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 debe tener exactamente 64 caracteres hexadecimales (32 bytes). Actual: ${key?.length || 0}`);
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 debe contener solo caracteres hexadecimales (0-9, a-f, A-F)');
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(`Error al cifrar: ${error.message}`);
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(`Error al descifrar: ${error.message}. Retornando texto original.`);
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: 'El valor debe ser una cadena de texto'
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: `El valor no cumple con el patrón de validación: ${attribute.regex}`
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: `Patrón regex inválido: ${error.message}`
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: `El valor excede la longitud máxima de ${attribute.maxLength} caracteres`
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: `El valor debe tener al menos ${attribute.minLength} caracteres`
136
+ error: `Value must be at least ${attribute.minLength} characters`
137
137
  };
138
138
  }
139
139