@growy/strapi-plugin-encrypted-field 2.3.3 → 2.4.1

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,101 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [2.4.1] - 2026-02-26
6
+
7
+ ### Changed
8
+ - Updated Node.js engine requirement to support version 23.x.x.
9
+
10
+ ## [2.4.0] - 2026-02-26
11
+
12
+ ### Added
13
+ - Key rotation script (`scripts/rotate-key.js`) for re-encrypting data when changing the encryption key.
14
+ - CHANGELOG.md with full version history.
15
+ - Documented `uniqueField` limitation in README (unique constraints don't work on encrypted fields due to random IV).
16
+
17
+ ### Changed
18
+ - All server-side error messages and logs translated to English for global developer audience.
19
+ - `description` fields in `package.json` translated to English.
20
+ - `repository.url` fixed via `npm pkg fix` (normalized to `git+https://` format).
21
+
22
+ ### Fixed
23
+ - `strapi.config.get()` path updated from deprecated `plugin.encrypted-field` to Strapi v5 convention `plugin::encrypted-field`.
24
+
25
+ ## [2.3.3] - 2026-02-26
26
+
27
+ ### Changed
28
+ - README rewritten in English for global npm community adoption.
29
+ - Removed unused `admin/src/pages/` directory.
30
+
31
+ ## [2.3.2] - 2026-02-26
32
+
33
+ ### Changed
34
+ - Restored detailed technical documentation in README (API examples, key management warnings, regex validation guide).
35
+
36
+ ## [2.3.1] - 2026-02-26
37
+
38
+ ### Changed
39
+ - README converted to bilingual format (EN/ES).
40
+ - Removed unused server directories (`content-types`, `controllers`, `routes`, `services`).
41
+ - Added `.npmignore` for cleaner npm package.
42
+
43
+ ## [2.3.0] - 2026-02-26
44
+
45
+ ### Added
46
+ - Multi-language support (i18n): English and Spanish translations for admin UI.
47
+ - Encryption key caching in memory for improved performance.
48
+ - `inputSize` configuration for resizable inputs in Content-Type Builder.
49
+ - Plugin `config` block with `default` and `validator` (Strapi v5 standard).
50
+ - `@strapi/design-system` and `react` as `peerDependencies`.
51
+
52
+ ### Changed
53
+ - Refactored lifecycle hooks (`bootstrap.js`): extracted shared `processEncryption` and `processDecryption` functions to eliminate code duplication.
54
+ - Simplified `registerTrads` to native Strapi v5 async/await pattern.
55
+
56
+ ### Fixed
57
+ - 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.
58
+
59
+ ## [2.2.1] - 2025-10-14
60
+
61
+ ### Added
62
+ - Visibility toggle (show/hide) and copy-to-clipboard controls with confirmation notifications in admin UI.
63
+
64
+ ### Changed
65
+ - Updated README with improved documentation and package metadata.
66
+
67
+ ## [2.0.4] - 2025-10-13
68
+
69
+ ### Removed
70
+ - Placeholder field option removed after multiple iterations.
71
+
72
+ ## [2.0.3] - 2025-10-13
73
+
74
+ ### Fixed
75
+ - Set predefined placeholder and removed customization option.
76
+
77
+ ## [2.0.1] - 2025-10-13
78
+
79
+ ### Added
80
+ - Nested component support for encryption/decryption at any depth.
81
+ - Decrypt middleware for API responses.
82
+ - Regex and length validation before encryption.
83
+
84
+ ### Changed
85
+ - Simplified encrypted field UI.
86
+ - Improved props handling in Input component.
87
+ - Field type changed to `string`.
88
+ - Removed unnecessary code comments.
89
+
90
+ ## [1.0.1] - 2025-10-13
91
+
92
+ ### Added
93
+ - Package exports configuration for Strapi v5.
94
+
95
+ ## [1.0.0] - 2025-10-13
96
+
97
+ ### Added
98
+ - Initial release.
99
+ - Custom field "Encrypted Text" for Content-Type Builder.
100
+ - AES-256-GCM encryption/decryption.
101
+ - Basic admin panel input component.
package/README.md CHANGED
@@ -201,12 +201,22 @@ apiKey: "sk-1234567890abcdef"
201
201
 
202
202
  ### Best Practices
203
203
 
204
- 1. **Key rotation**: Plan a periodic rotation process
204
+ 1. **Key rotation**: Use the included rotation script (see below)
205
205
  2. **Environment separation**: Use different keys per dev/staging/prod
206
206
  3. **Auditing**: Monitor encryption/decryption error logs
207
207
  4. **Key backup**: Keep secure copies of keys in multiple locations
208
208
  5. **Private fields**: Mark sensitive fields as "private" to exclude them from the public API
209
209
 
210
+ ### Key Rotation
211
+
212
+ If you need to change your encryption key, use the included rotation script to re-encrypt existing data:
213
+
214
+ ```bash
215
+ node scripts/rotate-key.js --old=<CURRENT_64_CHAR_KEY> --new=<NEW_64_CHAR_KEY>
216
+ ```
217
+
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.).
219
+
210
220
  ## Use Cases
211
221
 
212
222
  - 🔑 Third-party API Keys
@@ -221,6 +231,7 @@ apiKey: "sk-1234567890abcdef"
221
231
  - ❌ **Search**: Cannot search by encrypted fields (data is encrypted in DB)
222
232
  - ❌ **Sorting**: Cannot sort by encrypted fields
223
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)
224
235
  - ⚠️ **Performance**: Encryption/decryption adds minimal overhead (~1-2ms per operation)
225
236
  - ⚠️ **Key synchronization**: All environments sharing the same DB must use the same key
226
237
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@growy/strapi-plugin-encrypted-field",
3
- "version": "2.3.3",
4
- "description": "Campo personalizado de texto cifrado para Strapi",
3
+ "version": "2.4.1",
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
  {
@@ -35,7 +35,7 @@
35
35
  }
36
36
  ],
37
37
  "engines": {
38
- "node": ">=18.0.0 <=22.x.x",
38
+ "node": ">=18.0.0 <=23.x.x",
39
39
  "npm": ">=6.0.0"
40
40
  },
41
41
  "license": "MIT",
@@ -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