@growy/strapi-plugin-encrypted-field 2.2.1 → 2.3.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/README.md CHANGED
@@ -6,40 +6,41 @@
6
6
  <img src="https://img.shields.io/badge/Strapi-v5-blueviolet" alt="Strapi v5" />
7
7
  </div>
8
8
 
9
- 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.
9
+ ---
10
10
 
11
+ ### [English](#english-version) | [Español](#versión-en-español)
11
12
 
12
- - ✅ **Campo personalizado** "Texto Cifrado" en el Content-Type Builder
13
- - ✅ **Cifrado automático** AES-256-GCM al guardar
14
- - ✅ **Descifrado transparente** al leer (panel y API)
15
- - ✅ **Validación backend** con soporte para regex y restricciones
16
- - ✅ **UI mejorada** con controles de visibilidad y copiar al portapapeles
17
- - ✅ **Valores ocultos** por defecto con opción de mostrar/ocultar
18
- - ✅ **Notificaciones** de confirmación al copiar valores
19
- - ✅ **Gestión de claves robusta** con validación y mensajes de error claros
20
- - ✅ **Datos cifrados** en base de datos con IV único y Auth Tag
21
- - ✅ **Reutilizable** en cualquier colección o componente
22
- - ✅ **Soporte completo** para componentes anidados y estructuras complejas
13
+ ---
23
14
 
24
- ## Instalación
15
+ ## English Version
25
16
 
26
- ### Desde npm
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.
27
18
 
28
- ```bash
29
- npm install @growy/strapi-plugin-encrypted-field
30
- ```
19
+ ### Features
31
20
 
32
- ### Desde yarn
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 and copy to clipboard.
26
+ - ✅ **Multi-language support (i18n)**: English and Spanish.
27
+ - ✅ **Secure Key Management** with validation and clear error messages.
28
+ - ✅ **Encrypted Data** in database with unique IV and Auth Tag.
29
+ - ✅ **Nested Components support** at any depth.
30
+
31
+ ### Installation
33
32
 
34
33
  ```bash
34
+ npm install @growy/strapi-plugin-encrypted-field
35
+ # or
35
36
  yarn add @growy/strapi-plugin-encrypted-field
36
37
  ```
37
38
 
38
- ## Configuración
39
+ ### Configuration
39
40
 
40
- ### 1. Habilitar el plugin
41
+ #### 1. Enable the plugin
41
42
 
42
- Crea o edita `config/plugins.js` o `config/plugins.ts`:
43
+ Create or edit `config/plugins.js` or `config/plugins.ts`:
43
44
 
44
45
  ```javascript
45
46
  module.exports = {
@@ -49,199 +50,111 @@ module.exports = {
49
50
  };
50
51
  ```
51
52
 
52
- ### 2. Configurar la clave de cifrado (REQUERIDO)
53
+ #### 2. Configure Encryption Key (REQUIRED)
53
54
 
54
- #### Opción A: Variable de entorno (recomendado)
55
-
56
- Agrega a tu `.env`:
55
+ Add to your `.env`:
57
56
 
58
57
  ```bash
59
- ENCRYPTION_KEY=tu_clave_de_64_caracteres_hexadecimales_aqui
58
+ ENCRYPTION_KEY=your_64_character_hex_key_here
60
59
  ```
61
60
 
62
- #### Opción B: Archivo de configuración
63
-
64
- Edita `config/plugins.js`:
65
-
66
- ```javascript
67
- module.exports = ({ env }) => ({
68
- 'encrypted-field': {
69
- enabled: true,
70
- config: {
71
- encryptionKey: env('ENCRYPTION_KEY'),
72
- },
73
- },
74
- });
75
- ```
76
-
77
- #### Generar clave segura
78
-
61
+ Generate a secure key:
79
62
  ```bash
80
63
  node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
81
64
  ```
82
65
 
83
- Esto generará una clave de 64 caracteres hexadecimales (32 bytes).
84
-
85
- ⚠️ **CRÍTICO - Gestión de claves**:
86
- - **Guarda la clave de forma segura** (gestor de secretos, variables de entorno cifradas)
87
- - **Nunca** la incluyas en el control de versiones
88
- - **Si pierdes la clave**, no podrás descifrar los datos existentes
89
- - **Usa la misma clave** en todos los entornos que compartan la misma base de datos
90
- - **Para producción**, considera usar servicios como AWS Secrets Manager, HashiCorp Vault o similar
91
-
92
- ### 3. Rebuild del admin
93
-
94
- ```bash
95
- npm run build
96
- npm run develop
97
- ```
98
-
99
- ## Requisitos
100
-
101
- - **Strapi**: v5.0.0 o superior
102
- - **Node.js**: 18.x - 22.x
103
- - **npm**: 6.0.0 o superior
104
-
105
- ## Validación de datos
66
+ ### Usage
106
67
 
107
- El plugin soporta validación antes del cifrado:
68
+ 1. Go to **Content-Type Builder**.
69
+ 2. Select a collection or create a new one.
70
+ 3. Click on **"Add another field"**.
71
+ 4. Select **"Encrypted Text"** (look for the 🔒 icon).
72
+ 5. Values are hidden by default in the Admin Panel but can be revealed or copied using the inline buttons.
108
73
 
109
- ### Configurar validación regex
110
-
111
- 1. En el Content-Type Builder, selecciona el campo cifrado
112
- 2. Ve a la pestaña **"Advanced Settings"**
113
- 3. En **"RegEx pattern"**, ingresa tu expresión regular
114
- 4. Guarda los cambios
115
-
116
- **Ejemplo**: Para validar formato de API key:
117
- ```regex
118
- ^sk-[a-zA-Z0-9]{32}$
119
- ```
120
-
121
- Si el valor no cumple con el patrón, se lanzará un error antes de cifrar.
122
-
123
- ## Uso
124
-
125
- ### 1. Agregar campo cifrado a una colección
74
+ ---
126
75
 
127
- 1. Ve a **Content-Type Builder**
128
- 2. Selecciona una colección o crea una nueva
129
- 3. Click en **"Add another field"**
130
- 4. Busca **"Texto Cifrado"** (con icono 🔒)
131
- 5. Configura el nombre del campo
132
- 6. Guarda y reinicia Strapi
76
+ ## Versión en Español
133
77
 
134
- ### 2. Usar el campo
78
+ 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.
135
79
 
136
- El campo funciona como un campo de texto normal con características de seguridad adicionales:
80
+ ### Características
137
81
 
138
- - **En el panel**: Escribe texto normalmente
139
- - **Valores ocultos**: Los valores se muestran como `***` por defecto
140
- - **Botón ojo**: Alterna entre mostrar/ocultar el valor
141
- - **Botón copiar**: Copia el valor al portapapeles con notificación de confirmación
142
- - **Placeholder personalizable**: Configura un placeholder desde las opciones del campo
143
- - **Al guardar**: Se cifra automáticamente
144
- - **Al leer**: Se descifra automáticamente
145
- - **En la BD**: Se guarda cifrado con formato `iv:authTag:encrypted`
146
- - **En componentes**: Funciona igual en componentes anidados de cualquier profundidad
82
+ - **Campo personalizado** "Texto Cifrado" en el Content-Type Builder.
83
+ - **Cifrado automático** AES-256-GCM al guardar.
84
+ - **Descifrado transparente** al leer (panel y API).
85
+ - **Validación backend** con soporte para regex y restricciones.
86
+ - **UI Nativa Strapi v5** con controles de visibilidad y copiar al portapapeles.
87
+ - **Soporte multi-idioma (i18n)**: Inglés y Español.
88
+ - **Gestión de claves robusta** con validación y mensajes de error claros.
89
+ - **Datos cifrados** en base de datos con IV único y Auth Tag.
90
+ - **Soporte para componentes anidados** a cualquier profundidad.
147
91
 
148
- ### 3. Uso por API
92
+ ### Instalación
149
93
 
150
94
  ```bash
151
- # Crear con campo cifrado
152
- curl -X POST http://localhost:1337/api/usuarios \
153
- -H "Content-Type: application/json" \
154
- -d '{
155
- "data": {
156
- "nombre": "Juan",
157
- "apiKey": "mi-clave-secreta-123"
158
- }
159
- }'
160
-
161
- # Leer (devuelve descifrado)
162
- curl -X GET http://localhost:1337/api/usuarios/1
163
- # Response: { "nombre": "Juan", "apiKey": "mi-clave-secreta-123" }
95
+ npm install @growy/strapi-plugin-encrypted-field
96
+ # o
97
+ yarn add @growy/strapi-plugin-encrypted-field
164
98
  ```
165
99
 
166
- ## Ejemplo de uso
100
+ ### Configuración
167
101
 
168
- ### Colección "Usuario" con API Key cifrada
102
+ #### 1. Habilitar el plugin
169
103
 
170
- **Schema:**
171
- ```json
172
- {
173
- "nombre": "string",
174
- "email": "email",
175
- "apiKey": "plugin::encrypted-field.encrypted-text"
176
- }
177
- ```
178
-
179
- **En la BD:**
180
- ```
181
- apiKey: "a1b2c3d4e5f6....:f9e8d7c6b5a4....:9f8e7d6c5b4a3..."
182
- ```
104
+ Edita `config/plugins.js`:
183
105
 
184
- **En el panel y API:**
185
- ```
186
- apiKey: "sk-1234567890abcdef"
106
+ ```javascript
107
+ module.exports = {
108
+ 'encrypted-field': {
109
+ enabled: true,
110
+ },
111
+ };
187
112
  ```
188
113
 
189
- ## Seguridad y arquitectura
190
-
191
- ### Especificaciones técnicas
114
+ #### 2. Configurar la clave (REQUERIDO)
192
115
 
193
- - **Algoritmo**: AES-256-GCM (estándar NIST, grado militar)
194
- - **Tamaño de clave**: 256 bits (32 bytes, 64 caracteres hexadecimales)
195
- - **IV (Initialization Vector)**: 96 bits (12 bytes) generado aleatoriamente por operación
196
- - **Auth Tag**: 128 bits (16 bytes) para verificación de integridad
197
- - **Formato almacenado**: `iv:authTag:encryptedData` (todos en hexadecimal)
116
+ Agrega a tu `.env`:
198
117
 
199
- ### Características de seguridad
118
+ ```bash
119
+ ENCRYPTION_KEY=tu_clave_de_64_caracteres_hexadecimales_aqui
120
+ ```
200
121
 
201
- - ✅ **Cifrado autenticado**: GCM proporciona confidencialidad e integridad
202
- - ✅ **IV único**: Cada operación de cifrado genera un IV aleatorio
203
- - ✅ **Resistencia a manipulación**: Auth Tag detecta cualquier modificación
204
- - ✅ **Validación de entrada**: Soporte para regex y restricciones personalizadas
205
- - ✅ **Manejo de errores seguro**: Logs controlados sin exponer datos sensibles
122
+ ### Uso
206
123
 
207
- ### Mejores prácticas
124
+ 1. Ve a **Content-Type Builder**.
125
+ 2. Selecciona una colección.
126
+ 3. Click en **"Add another field"**.
127
+ 4. Selecciona **"Texto Cifrado"** (icono 🔒).
128
+ 5. Los valores están ocultos por defecto en el panel pero pueden mostrarse o copiarse con los botones integrados.
208
129
 
209
- 1. **Rotación de claves**: Planifica un proceso de rotación periódica
210
- 2. **Separación de entornos**: Usa claves diferentes para dev/staging/prod
211
- 3. **Auditoría**: Monitorea logs de errores de cifrado/descifrado
212
- 4. **Backup de claves**: Mantén copias seguras de las claves en múltiples ubicaciones
213
- 5. **Campos privados**: Marca campos sensibles como "privados" para excluirlos de la API pública
130
+ ### Seguridad y Especificaciones
214
131
 
215
- ## Casos de uso
132
+ - **Algoritmo**: AES-256-GCM.
133
+ - **IV (Initialization Vector)**: 96 bits generado aleatoriamente por operación.
134
+ - **Auth Tag**: 128 bits para verificación de integridad.
135
+ - **Formato almacenado**: `iv:authTag:encryptedData`.
216
136
 
217
- - 🔑 API Keys de terceros
218
- - 🔐 Tokens de acceso
219
- - 🔒 Secretos de webhooks
220
- - 💳 Información sensible
221
- - 📧 Credenciales SMTP
222
- - 🔑 Contraseñas de aplicaciones
137
+ ### Limitaciones
223
138
 
224
- ## Limitaciones conocidas
139
+ - **Búsqueda**: No se puede buscar por campos cifrados.
140
+ - ❌ **Ordenamiento**: No se puede ordenar por campos cifrados.
141
+ - ❌ **Filtros**: No se pueden aplicar filtros directos sobre datos cifrados.
225
142
 
226
- - ❌ **Búsqueda**: No se puede buscar por campos cifrados (datos cifrados en BD)
227
- - ❌ **Ordenamiento**: No se puede ordenar por campos cifrados
228
- - ❌ **Filtros**: No se pueden aplicar filtros directos sobre campos cifrados
229
- - ⚠️ **Rendimiento**: El cifrado/descifrado añade overhead mínimo (~1-2ms por operación)
230
- - ⚠️ **Sincronización de claves**: Todos los entornos que compartan BD deben usar la misma clave
143
+ ---
231
144
 
232
- ## Licencia
145
+ ## License / Licencia
233
146
 
234
147
  MIT © 2025 Growy AI
235
148
 
236
- ## Desarrollado por
149
+ ## Credits / Créditos
237
150
 
238
151
  **Growy AI** - Soluciones de IA y automatización empresarial
239
152
 
240
- **Autor principal**: Zahir El isaac
153
+ **Main Author / Autor principal**: Zahir El isaac
241
154
 
242
155
  ---
243
156
 
244
157
  <div align="center">
245
- <p>Si este plugin te resulta útil, considera darle una ⭐ en GitHub</p>
246
- <p>Hecho con ❤️ por el equipo de Growy AI</p>
158
+ <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>
159
+ <p>Made with ❤️ by Growy AI Team</p>
247
160
  </div>
@@ -34,29 +34,45 @@ const Input = (props) => {
34
34
  };
35
35
 
36
36
  const handleCopy = async () => {
37
- if (value) {
38
- try {
39
- await navigator.clipboard.writeText(value);
40
- toggleNotification({
41
- type: 'success',
42
- message: 'Copiado al portapapeles',
43
- });
44
- } catch (err) {
45
- toggleNotification({
46
- type: 'danger',
47
- message: 'Error al copiar',
48
- });
49
- }
37
+ if (!value) return;
38
+
39
+ try {
40
+ await navigator.clipboard.writeText(value);
41
+ toggleNotification({
42
+ type: 'success',
43
+ message: formatMessage({
44
+ id: 'encrypted-field.notification.copied',
45
+ defaultMessage: 'Copiado al portapapeles',
46
+ }),
47
+ });
48
+ } catch (err) {
49
+ toggleNotification({
50
+ type: 'danger',
51
+ message: formatMessage({
52
+ id: 'encrypted-field.notification.copyError',
53
+ defaultMessage: 'Error al copiar',
54
+ }),
55
+ });
50
56
  }
51
57
  };
52
58
 
53
59
  const toggleVisibility = () => {
54
- setIsVisible(!isVisible);
60
+ setIsVisible((prev) => !prev);
55
61
  };
56
62
 
57
63
  const fieldName = name.includes('.') ? name.split('.').pop() : name;
58
64
  const label = intlLabel?.id ? formatMessage(intlLabel) : (intlLabel || fieldName);
59
65
 
66
+ const visibilityLabel = formatMessage({
67
+ id: isVisible ? 'encrypted-field.action.hide' : 'encrypted-field.action.show',
68
+ defaultMessage: isVisible ? 'Ocultar' : 'Mostrar',
69
+ });
70
+
71
+ const copyLabel = formatMessage({
72
+ id: 'encrypted-field.action.copy',
73
+ defaultMessage: 'Copiar',
74
+ });
75
+
60
76
  return (
61
77
  <Field.Root
62
78
  name={name}
@@ -77,17 +93,17 @@ const Input = (props) => {
77
93
  disabled={disabled}
78
94
  style={{ paddingRight: '80px' }}
79
95
  />
80
- <div style={{
81
- position: 'absolute',
82
- right: '8px',
83
- top: '50%',
96
+ <div style={{
97
+ position: 'absolute',
98
+ right: '8px',
99
+ top: '50%',
84
100
  transform: 'translateY(-50%)',
85
101
  display: 'flex',
86
- gap: '4px'
102
+ gap: '4px',
87
103
  }}>
88
104
  <IconButton
89
105
  onClick={toggleVisibility}
90
- label={isVisible ? 'Ocultar' : 'Mostrar'}
106
+ label={visibilityLabel}
91
107
  disabled={disabled}
92
108
  variant="ghost"
93
109
  >
@@ -95,7 +111,7 @@ const Input = (props) => {
95
111
  </IconButton>
96
112
  <IconButton
97
113
  onClick={handleCopy}
98
- label="Copiar"
114
+ label={copyLabel}
99
115
  disabled={disabled || !value}
100
116
  variant="ghost"
101
117
  >
@@ -103,24 +103,15 @@ export default {
103
103
  },
104
104
 
105
105
  async registerTrads({ locales }) {
106
- const importedTrads = await Promise.all(
107
- locales.map((locale) => {
108
- return import(`./translations/${locale}.json`)
109
- .then(({ default: data }) => {
110
- return {
111
- data,
112
- locale,
113
- };
114
- })
115
- .catch(() => {
116
- return {
117
- data: {},
118
- locale,
119
- };
120
- });
106
+ return Promise.all(
107
+ locales.map(async (locale) => {
108
+ try {
109
+ const { default: data } = await import(`./translations/${locale}.json`);
110
+ return { data, locale };
111
+ } catch {
112
+ return { data: {}, locale };
113
+ }
121
114
  })
122
115
  );
123
-
124
- return Promise.resolve(importedTrads);
125
116
  },
126
117
  };
@@ -13,5 +13,10 @@
13
13
  "encrypted-field.options.minLength.description": "Minimum number of characters required",
14
14
  "encrypted-field.options.advanced.regex": "Validation",
15
15
  "encrypted-field.options.regex.label": "RegEx pattern",
16
- "encrypted-field.options.regex.description": "Validation pattern before encryption"
17
- }
16
+ "encrypted-field.options.regex.description": "Validation pattern before encryption",
17
+ "encrypted-field.notification.copied": "Copied to clipboard",
18
+ "encrypted-field.notification.copyError": "Error copying to clipboard",
19
+ "encrypted-field.action.show": "Show",
20
+ "encrypted-field.action.hide": "Hide",
21
+ "encrypted-field.action.copy": "Copy"
22
+ }
@@ -13,5 +13,10 @@
13
13
  "encrypted-field.options.minLength.description": "Número mínimo de caracteres requeridos",
14
14
  "encrypted-field.options.advanced.regex": "Validación",
15
15
  "encrypted-field.options.regex.label": "RegEx pattern",
16
- "encrypted-field.options.regex.description": "Patrón de validación antes de cifrar"
17
- }
16
+ "encrypted-field.options.regex.description": "Patrón de validación antes de cifrar",
17
+ "encrypted-field.notification.copied": "Copiado al portapapeles",
18
+ "encrypted-field.notification.copyError": "Error al copiar al portapapeles",
19
+ "encrypted-field.action.show": "Mostrar",
20
+ "encrypted-field.action.hide": "Ocultar",
21
+ "encrypted-field.action.copy": "Copiar"
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growy/strapi-plugin-encrypted-field",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
4
4
  "description": "Campo personalizado de texto cifrado para Strapi",
5
5
  "strapi": {
6
6
  "name": "encrypted-field",
@@ -10,7 +10,9 @@
10
10
  },
11
11
  "dependencies": {},
12
12
  "peerDependencies": {
13
- "@strapi/strapi": "^5.0.0"
13
+ "@strapi/strapi": "^5.0.0",
14
+ "@strapi/design-system": "^2.0.0",
15
+ "react": "^18.0.0"
14
16
  },
15
17
  "main": "server/index.js",
16
18
  "exports": {
@@ -53,4 +55,4 @@
53
55
  "publishConfig": {
54
56
  "access": "public"
55
57
  }
56
- }
58
+ }
@@ -1,117 +1,79 @@
1
1
  const { encrypt, decrypt, validateValue, isEncryptedField } = require('./utils/crypto');
2
2
 
3
+ function processEncryption(event, strapi) {
4
+ if (!event.model?.uid) return;
5
+
6
+ const { data } = event.params;
7
+ const currentModel = strapi.getModel(event.model.uid);
8
+
9
+ if (!currentModel?.attributes) return;
10
+
11
+ for (const [key, attribute] of Object.entries(currentModel.attributes)) {
12
+ if (!isEncryptedField(attribute)) continue;
13
+ if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
14
+
15
+ const value = data[key];
16
+ if (value === null || value === undefined || value === '') continue;
17
+
18
+ const validation = validateValue(value, attribute);
19
+ if (!validation.valid) {
20
+ throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
21
+ }
22
+
23
+ data[key] = encrypt(value, strapi);
24
+ }
25
+ }
26
+
27
+ function processDecryption(result, event, strapi) {
28
+ if (!result) return;
29
+ if (!event.model?.uid) return;
30
+
31
+ const currentModel = strapi.getModel(event.model.uid);
32
+ if (!currentModel?.attributes) return;
33
+
34
+ for (const [key, attribute] of Object.entries(currentModel.attributes)) {
35
+ if (!isEncryptedField(attribute)) continue;
36
+ if (!Object.prototype.hasOwnProperty.call(result, key)) continue;
37
+
38
+ const value = result[key];
39
+ if (typeof value === 'string' && value) {
40
+ result[key] = decrypt(value, strapi);
41
+ }
42
+ }
43
+ }
44
+
3
45
  module.exports = ({ strapi }) => {
4
-
5
-
6
- const contentTypes = Object.values(strapi.contentTypes);
7
- const components = Object.values(strapi.components);
8
- const allModels = [...contentTypes, ...components];
9
-
46
+ const allModels = [
47
+ ...Object.values(strapi.contentTypes),
48
+ ...Object.values(strapi.components),
49
+ ];
50
+
10
51
  allModels.forEach((model) => {
11
52
  const attributes = model.attributes || {};
12
-
13
- const encryptedFields = Object.entries(attributes)
14
- .filter(([key, attr]) => isEncryptedField(attr))
15
- .map(([key]) => key);
16
-
17
- if (encryptedFields.length === 0) return;
53
+ const hasEncryptedFields = Object.values(attributes).some(isEncryptedField);
18
54
 
19
- const uid = model.uid;
55
+ if (!hasEncryptedFields) return;
20
56
 
21
-
22
57
  strapi.db.lifecycles.subscribe({
23
- models: [uid],
24
-
58
+ models: [model.uid],
59
+
25
60
  async beforeCreate(event) {
26
- const { data } = event.params;
27
-
28
- if (!event.model?.uid) return;
29
-
30
- const currentModel = strapi.getModel(event.model.uid);
31
-
32
- if (!currentModel?.attributes) return;
33
-
34
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
35
- if (!isEncryptedField(attribute)) continue;
36
-
37
- if (Object.prototype.hasOwnProperty.call(data, key)) {
38
- const value = data[key];
39
-
40
- if (value === null || value === undefined || value === '') continue;
41
-
42
- const validation = validateValue(value, attribute);
43
- if (!validation.valid) {
44
- throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
45
- }
46
-
47
- data[key] = encrypt(value, strapi);
48
- }
49
- }
61
+ processEncryption(event, strapi);
50
62
  },
51
63
 
52
64
  async beforeUpdate(event) {
53
- if (!event.model?.uid) return;
54
-
55
- const { data } = event.params;
56
- const currentModel = strapi.getModel(event.model.uid);
57
-
58
- if (!currentModel?.attributes) return;
59
-
60
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
61
- if (!isEncryptedField(attribute)) continue;
62
- if (Object.prototype.hasOwnProperty.call(data, key)) {
63
- const value = data[key];
64
- if (value === null || value === undefined || value === '') continue;
65
-
66
- const validation = validateValue(value, attribute);
67
- if (!validation.valid) {
68
- throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
69
- }
70
-
71
- data[key] = encrypt(value, strapi);
72
- }
73
- }
65
+ processEncryption(event, strapi);
74
66
  },
75
67
 
76
68
  async afterFindOne(event) {
77
- const { result } = event;
78
- if (!result) return;
79
- if (!event.model?.uid) return;
80
-
81
- const currentModel = strapi.getModel(event.model.uid);
82
-
83
- if (!currentModel?.attributes) return;
84
-
85
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
86
- if (!isEncryptedField(attribute)) continue;
87
- if (Object.prototype.hasOwnProperty.call(result, key)) {
88
- const value = result[key];
89
- if (typeof value === 'string' && value) {
90
- result[key] = decrypt(value, strapi);
91
- }
92
- }
93
- }
69
+ processDecryption(event.result, event, strapi);
94
70
  },
95
71
 
96
72
  async afterFindMany(event) {
97
73
  const { result } = event;
98
74
  if (!result || !Array.isArray(result)) return;
99
- if (!event.model?.uid) return;
100
-
101
- const currentModel = strapi.getModel(event.model.uid);
102
-
103
- if (!currentModel?.attributes) return;
104
-
105
75
  for (const item of result) {
106
- for (const [key, attribute] of Object.entries(currentModel.attributes)) {
107
- if (!isEncryptedField(attribute)) continue;
108
- if (Object.prototype.hasOwnProperty.call(item, key)) {
109
- const value = item[key];
110
- if (typeof value === 'string' && value) {
111
- item[key] = decrypt(value, strapi);
112
- }
113
- }
114
- }
76
+ processDecryption(item, event, strapi);
115
77
  }
116
78
  },
117
79
  });
package/server/index.js CHANGED
@@ -5,6 +5,10 @@ const decrypt = require('./middlewares/decrypt');
5
5
  module.exports = {
6
6
  register,
7
7
  bootstrap,
8
+ config: {
9
+ default: {},
10
+ validator: () => { },
11
+ },
8
12
  middlewares: {
9
13
  decrypt,
10
14
  },
@@ -1,38 +1,49 @@
1
1
  const { decrypt, isEncryptedField } = require('../utils/crypto');
2
2
 
3
+ function resolveModelUidFromPath(ctx, strapi) {
4
+ const path = ctx.request?.path || '';
5
+ const match = path.match(/^\/api\/([^/]+)/);
6
+ if (!match) return null;
7
+
8
+ const slug = match[1];
9
+ const contentTypes = Object.values(strapi.contentTypes);
10
+
11
+ const ct = contentTypes.find((ct) => {
12
+ const info = ct.info || {};
13
+ return info.pluralName === slug || info.singularName === slug;
14
+ });
15
+
16
+ return ct?.uid || null;
17
+ }
18
+
3
19
  module.exports = (config, { strapi }) => {
4
20
  return async (ctx, next) => {
5
21
  await next();
6
22
 
7
23
  if (!ctx.body) return;
8
24
 
9
-
25
+ const rootModelUid = resolveModelUidFromPath(ctx, strapi);
26
+
10
27
  const decryptRecursive = (obj, modelUid = null) => {
11
28
  if (!obj || typeof obj !== 'object') return;
12
29
 
13
-
14
30
  if (Array.isArray(obj)) {
15
- obj.forEach(item => decryptRecursive(item, modelUid));
31
+ obj.forEach((item) => decryptRecursive(item, modelUid));
16
32
  return;
17
33
  }
18
34
 
19
-
20
35
  let currentModelUid = modelUid;
21
36
  if (obj.__component) {
22
37
  currentModelUid = obj.__component;
23
38
  }
24
39
 
25
-
26
40
  let model = null;
27
41
  if (currentModelUid) {
28
42
  try {
29
43
  model = strapi.getModel(currentModelUid) || strapi.components[currentModelUid];
30
- } catch (e) {
31
-
32
- }
44
+ } catch (e) { }
33
45
  }
34
46
 
35
-
36
47
  if (model?.attributes) {
37
48
  for (const [key, attribute] of Object.entries(model.attributes)) {
38
49
  if (isEncryptedField(attribute) && obj[key] && typeof obj[key] === 'string') {
@@ -45,7 +56,6 @@ module.exports = (config, { strapi }) => {
45
56
  }
46
57
  }
47
58
 
48
-
49
59
  for (const value of Object.values(obj)) {
50
60
  if (value && typeof value === 'object') {
51
61
  decryptRecursive(value, currentModelUid);
@@ -53,12 +63,10 @@ module.exports = (config, { strapi }) => {
53
63
  }
54
64
  };
55
65
 
56
-
57
66
  if (ctx.body.data) {
58
- decryptRecursive(ctx.body.data);
67
+ decryptRecursive(ctx.body.data, rootModelUid);
59
68
  } else {
60
-
61
- decryptRecursive(ctx.body);
69
+ decryptRecursive(ctx.body, rootModelUid);
62
70
  }
63
71
  };
64
72
  };
@@ -1,11 +1,15 @@
1
1
  module.exports = ({ strapi }) => {
2
2
  const decryptMiddleware = require('./middlewares/decrypt');
3
-
3
+
4
4
  strapi.server.use(decryptMiddleware({}, { strapi }));
5
-
5
+
6
6
  strapi.customFields.register({
7
7
  name: 'encrypted-text',
8
8
  plugin: 'encrypted-field',
9
9
  type: 'string',
10
+ inputSize: {
11
+ default: 6,
12
+ isResizable: true,
13
+ },
10
14
  });
11
15
  };
@@ -5,9 +5,12 @@ const IV_LENGTH = 12;
5
5
  const AUTH_TAG_LENGTH = 16;
6
6
  const KEY_LENGTH = 32;
7
7
 
8
+ let _cachedKey = null;
9
+ let _cachedKeySource = null;
10
+
8
11
  function getEncryptionKey(strapi) {
9
12
  const key = process.env.ENCRYPTION_KEY || strapi?.config?.get('plugin.encrypted-field.encryptionKey');
10
-
13
+
11
14
  if (!key) {
12
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.';
13
16
  if (strapi?.log?.error) {
@@ -15,33 +18,39 @@ function getEncryptionKey(strapi) {
15
18
  }
16
19
  throw new Error(errorMsg);
17
20
  }
18
-
21
+
22
+ if (_cachedKey && _cachedKeySource === key) {
23
+ return _cachedKey;
24
+ }
25
+
19
26
  if (typeof key !== 'string' || key.length !== 64) {
20
27
  throw new Error(`ENCRYPTION_KEY debe tener exactamente 64 caracteres hexadecimales (32 bytes). Actual: ${key?.length || 0}`);
21
28
  }
22
-
29
+
23
30
  if (!/^[0-9a-fA-F]{64}$/.test(key)) {
24
31
  throw new Error('ENCRYPTION_KEY debe contener solo caracteres hexadecimales (0-9, a-f, A-F)');
25
32
  }
26
-
27
- return Buffer.from(key, 'hex');
33
+
34
+ _cachedKey = Buffer.from(key, 'hex');
35
+ _cachedKeySource = key;
36
+ return _cachedKey;
28
37
  }
29
38
 
30
39
  function encrypt(text, strapi) {
31
40
  if (typeof text !== 'string') return text;
32
-
41
+
33
42
  if (text === '') return text;
34
43
 
35
44
  try {
36
45
  const ENCRYPTION_KEY = getEncryptionKey(strapi);
37
46
  const iv = crypto.randomBytes(IV_LENGTH);
38
47
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
39
-
48
+
40
49
  let encrypted = cipher.update(text, 'utf8', 'hex');
41
50
  encrypted += cipher.final('hex');
42
-
51
+
43
52
  const authTag = cipher.getAuthTag();
44
-
53
+
45
54
  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
46
55
  } catch (error) {
47
56
  if (strapi?.log?.error) {
@@ -53,29 +62,29 @@ function encrypt(text, strapi) {
53
62
 
54
63
  function decrypt(encryptedText, strapi) {
55
64
  if (!encryptedText || typeof encryptedText !== 'string') return encryptedText;
56
-
65
+
57
66
  const parts = encryptedText.split(':');
58
67
  if (parts.length !== 3) {
59
68
  return encryptedText;
60
69
  }
61
-
70
+
62
71
  try {
63
72
  const [ivHex, authTagHex, encrypted] = parts;
64
-
73
+
65
74
  if (ivHex.length !== IV_LENGTH * 2 || authTagHex.length !== AUTH_TAG_LENGTH * 2) {
66
75
  return encryptedText;
67
76
  }
68
-
77
+
69
78
  const ENCRYPTION_KEY = getEncryptionKey(strapi);
70
79
  const iv = Buffer.from(ivHex, 'hex');
71
80
  const authTag = Buffer.from(authTagHex, 'hex');
72
-
81
+
73
82
  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
74
83
  decipher.setAuthTag(authTag);
75
-
84
+
76
85
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
77
86
  decrypted += decipher.final('utf8');
78
-
87
+
79
88
  return decrypted;
80
89
  } catch (error) {
81
90
  if (strapi?.log?.debug) {
@@ -91,9 +100,9 @@ function validateValue(value, attribute) {
91
100
  }
92
101
 
93
102
  if (typeof value !== 'string') {
94
- return {
95
- valid: false,
96
- error: 'El valor debe ser una cadena de texto'
103
+ return {
104
+ valid: false,
105
+ error: 'El valor debe ser una cadena de texto'
97
106
  };
98
107
  }
99
108
 
@@ -101,30 +110,30 @@ function validateValue(value, attribute) {
101
110
  try {
102
111
  const regex = new RegExp(attribute.regex);
103
112
  if (!regex.test(value)) {
104
- return {
105
- valid: false,
106
- error: `El valor no cumple con el patrón de validación: ${attribute.regex}`
113
+ return {
114
+ valid: false,
115
+ error: `El valor no cumple con el patrón de validación: ${attribute.regex}`
107
116
  };
108
117
  }
109
118
  } catch (error) {
110
- return {
111
- valid: false,
112
- error: `Patrón regex inválido: ${error.message}`
119
+ return {
120
+ valid: false,
121
+ error: `Patrón regex inválido: ${error.message}`
113
122
  };
114
123
  }
115
124
  }
116
125
 
117
126
  if (attribute.maxLength && value.length > attribute.maxLength) {
118
- return {
119
- valid: false,
120
- error: `El valor excede la longitud máxima de ${attribute.maxLength} caracteres`
127
+ return {
128
+ valid: false,
129
+ error: `El valor excede la longitud máxima de ${attribute.maxLength} caracteres`
121
130
  };
122
131
  }
123
132
 
124
133
  if (attribute.minLength && value.length < attribute.minLength) {
125
- return {
126
- valid: false,
127
- error: `El valor debe tener al menos ${attribute.minLength} caracteres`
134
+ return {
135
+ valid: false,
136
+ error: `El valor debe tener al menos ${attribute.minLength} caracteres`
128
137
  };
129
138
  }
130
139