@growy/strapi-plugin-encrypted-field 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,248 @@
1
+ # Strapi Plugin - Encrypted Field
2
+
3
+ <div align="center">
4
+ <img src="https://img.shields.io/npm/v/@growy/strapi-plugin-encrypted-field" alt="npm version" />
5
+ <img src="https://img.shields.io/npm/l/@growy/strapi-plugin-encrypted-field" alt="license" />
6
+ <img src="https://img.shields.io/badge/Strapi-v5-blueviolet" alt="Strapi v5" />
7
+ </div>
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.
10
+
11
+ ## Características
12
+
13
+ - ✅ **Campo personalizado** "Texto Cifrado" en el Content-Type Builder
14
+ - ✅ **Cifrado automático** AES-256-GCM al guardar
15
+ - ✅ **Descifrado transparente** al leer (panel y API)
16
+ - ✅ **Validación backend** con soporte para regex y restricciones
17
+ - ✅ **UI mejorada** con controles de visibilidad, copiar y contador de caracteres
18
+ - ✅ **Gestión de claves robusta** con validación y mensajes de error claros
19
+ - ✅ **Datos cifrados** en base de datos con IV único y Auth Tag
20
+ - ✅ **Reutilizable** en cualquier colección o componente
21
+
22
+ ## Instalación
23
+
24
+ ### Desde npm
25
+
26
+ ```bash
27
+ npm install @growy/strapi-plugin-encrypted-field
28
+ ```
29
+
30
+ ### Desde yarn
31
+
32
+ ```bash
33
+ yarn add @growy/strapi-plugin-encrypted-field
34
+ ```
35
+
36
+ ## Configuración
37
+
38
+ ### 1. Habilitar el plugin
39
+
40
+ Crea o edita `config/plugins.js` o `config/plugins.ts`:
41
+
42
+ ```javascript
43
+ module.exports = {
44
+ 'encrypted-field': {
45
+ enabled: true,
46
+ },
47
+ };
48
+ ```
49
+
50
+ ### 2. Configurar la clave de cifrado (REQUERIDO)
51
+
52
+ #### Opción A: Variable de entorno (recomendado)
53
+
54
+ Agrega a tu `.env`:
55
+
56
+ ```bash
57
+ ENCRYPTION_KEY=tu_clave_de_64_caracteres_hexadecimales_aqui
58
+ ```
59
+
60
+ #### Opción B: Archivo de configuración
61
+
62
+ Edita `config/plugins.js`:
63
+
64
+ ```javascript
65
+ module.exports = ({ env }) => ({
66
+ 'encrypted-field': {
67
+ enabled: true,
68
+ config: {
69
+ encryptionKey: env('ENCRYPTION_KEY'),
70
+ },
71
+ },
72
+ });
73
+ ```
74
+
75
+ #### Generar clave segura
76
+
77
+ ```bash
78
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
79
+ ```
80
+
81
+ Esto generará una clave de 64 caracteres hexadecimales (32 bytes).
82
+
83
+ ⚠️ **CRÍTICO - Gestión de claves**:
84
+ - **Guarda la clave de forma segura** (gestor de secretos, variables de entorno cifradas)
85
+ - **Nunca** la incluyas en el control de versiones
86
+ - **Si pierdes la clave**, no podrás descifrar los datos existentes
87
+ - **Usa la misma clave** en todos los entornos que compartan la misma base de datos
88
+ - **Para producción**, considera usar servicios como AWS Secrets Manager, HashiCorp Vault o similar
89
+
90
+ ### 3. Rebuild del admin
91
+
92
+ ```bash
93
+ npm run build
94
+ npm run develop
95
+ ```
96
+
97
+ ## Requisitos
98
+
99
+ - **Strapi**: v5.0.0 o superior
100
+ - **Node.js**: 18.x - 22.x
101
+ - **npm**: 6.0.0 o superior
102
+
103
+ ## Validación de datos
104
+
105
+ El plugin soporta validación antes del cifrado:
106
+
107
+ ### Configurar validación regex
108
+
109
+ 1. En el Content-Type Builder, selecciona el campo cifrado
110
+ 2. Ve a la pestaña **"Advanced Settings"**
111
+ 3. En **"RegEx pattern"**, ingresa tu expresión regular
112
+ 4. Guarda los cambios
113
+
114
+ **Ejemplo**: Para validar formato de API key:
115
+ ```regex
116
+ ^sk-[a-zA-Z0-9]{32}$
117
+ ```
118
+
119
+ Si el valor no cumple con el patrón, se lanzará un error antes de cifrar.
120
+
121
+ ## Uso
122
+
123
+ ### 1. Agregar campo cifrado a una colección
124
+
125
+ 1. Ve a **Content-Type Builder**
126
+ 2. Selecciona una colección o crea una nueva
127
+ 3. Click en **"Add another field"**
128
+ 4. Busca **"Texto Cifrado"** (con icono 🔒)
129
+ 5. Configura el nombre del campo
130
+ 6. Guarda y reinicia Strapi
131
+
132
+ ### 2. Usar el campo
133
+
134
+ El campo funciona como un campo de texto normal:
135
+
136
+ - **En el panel**: Escribe texto normalmente
137
+ - **Al guardar**: Se cifra automáticamente
138
+ - **Al leer**: Se descifra automáticamente
139
+ - **En la BD**: Se guarda cifrado con formato `iv:authTag:encrypted`
140
+
141
+ ### 3. Uso por API
142
+
143
+ ```bash
144
+ # Crear con campo cifrado
145
+ curl -X POST http://localhost:1337/api/usuarios \
146
+ -H "Content-Type: application/json" \
147
+ -d '{
148
+ "data": {
149
+ "nombre": "Juan",
150
+ "apiKey": "mi-clave-secreta-123"
151
+ }
152
+ }'
153
+
154
+ # Leer (devuelve descifrado)
155
+ curl -X GET http://localhost:1337/api/usuarios/1
156
+ # Response: { "nombre": "Juan", "apiKey": "mi-clave-secreta-123" }
157
+ ```
158
+
159
+ ## Ejemplo de uso
160
+
161
+ ### Colección "Usuario" con API Key cifrada
162
+
163
+ **Schema:**
164
+ ```json
165
+ {
166
+ "nombre": "string",
167
+ "email": "email",
168
+ "apiKey": "plugin::encrypted-field.encrypted-text"
169
+ }
170
+ ```
171
+
172
+ **En la BD:**
173
+ ```
174
+ apiKey: "a1b2c3d4e5f6....:f9e8d7c6b5a4....:9f8e7d6c5b4a3..."
175
+ ```
176
+
177
+ **En el panel y API:**
178
+ ```
179
+ apiKey: "sk-1234567890abcdef"
180
+ ```
181
+
182
+ ## Seguridad y arquitectura
183
+
184
+ ### Especificaciones técnicas
185
+
186
+ - **Algoritmo**: AES-256-GCM (estándar NIST, grado militar)
187
+ - **Tamaño de clave**: 256 bits (32 bytes, 64 caracteres hexadecimales)
188
+ - **IV (Initialization Vector)**: 96 bits (12 bytes) generado aleatoriamente por operación
189
+ - **Auth Tag**: 128 bits (16 bytes) para verificación de integridad
190
+ - **Formato almacenado**: `iv:authTag:encryptedData` (todos en hexadecimal)
191
+
192
+ ### Características de seguridad
193
+
194
+ - ✅ **Cifrado autenticado**: GCM proporciona confidencialidad e integridad
195
+ - ✅ **IV único**: Cada operación de cifrado genera un IV aleatorio
196
+ - ✅ **Resistencia a manipulación**: Auth Tag detecta cualquier modificación
197
+ - ✅ **Validación de entrada**: Soporte para regex y restricciones personalizadas
198
+ - ✅ **Manejo de errores seguro**: Logs controlados sin exponer datos sensibles
199
+
200
+ ### Mejores prácticas
201
+
202
+ 1. **Rotación de claves**: Planifica un proceso de rotación periódica
203
+ 2. **Separación de entornos**: Usa claves diferentes para dev/staging/prod
204
+ 3. **Auditoría**: Monitorea logs de errores de cifrado/descifrado
205
+ 4. **Backup de claves**: Mantén copias seguras de las claves en múltiples ubicaciones
206
+ 5. **Campos privados**: Marca campos sensibles como "privados" para excluirlos de la API pública
207
+
208
+ ## Casos de uso
209
+
210
+ - 🔑 API Keys de terceros
211
+ - 🔐 Tokens de acceso
212
+ - 🔒 Secretos de webhooks
213
+ - 💳 Información sensible
214
+ - 📧 Credenciales SMTP
215
+ - 🔑 Contraseñas de aplicaciones
216
+
217
+ ## Limitaciones conocidas
218
+
219
+ - ❌ **Búsqueda**: No se puede buscar por campos cifrados (datos cifrados en BD)
220
+ - ❌ **Ordenamiento**: No se puede ordenar por campos cifrados
221
+ - ❌ **Filtros**: No se pueden aplicar filtros directos sobre campos cifrados
222
+ - ⚠️ **Rendimiento**: El cifrado/descifrado añade overhead mínimo (~1-2ms por operación)
223
+ - ⚠️ **Sincronización de claves**: Todos los entornos que compartan BD deben usar la misma clave
224
+
225
+ ## Soporte
226
+
227
+ Para reportar bugs, solicitar funcionalidades o contribuir:
228
+
229
+ - **Issues**: [GitHub Issues](https://github.com/growy-ai/strapi-plugin-encrypted-field/issues)
230
+ - **Documentación**: [README.md](https://github.com/growy-ai/strapi-plugin-encrypted-field)
231
+ - **Email**: support@growy.ai
232
+
233
+ ## Licencia
234
+
235
+ MIT © 2025 Growy AI
236
+
237
+ ## Desarrollado por
238
+
239
+ **Growy AI** - Soluciones de IA y automatización empresarial
240
+
241
+ **Autor principal**: Zahir El isaac
242
+
243
+ ---
244
+
245
+ <div align="center">
246
+ <p>Si este plugin te resulta útil, considera darle una ⭐ en GitHub</p>
247
+ <p>Hecho con ❤️ por el equipo de Growy AI</p>
248
+ </div>
@@ -0,0 +1,92 @@
1
+ import React, { useState } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import { Field, Flex, IconButton, Typography } from '@strapi/design-system';
4
+ import { Eye, EyeStriked, Duplicate, Check } from '@strapi/icons';
5
+
6
+ const Input = ({
7
+ attribute,
8
+ description,
9
+ disabled,
10
+ error,
11
+ intlLabel,
12
+ labelAction,
13
+ name,
14
+ onChange,
15
+ required,
16
+ value,
17
+ }) => {
18
+ const { formatMessage } = useIntl();
19
+ const [showValue, setShowValue] = useState(false);
20
+ const [copied, setCopied] = useState(false);
21
+
22
+ const handleCopy = async () => {
23
+ if (value) {
24
+ try {
25
+ await navigator.clipboard.writeText(value);
26
+ setCopied(true);
27
+ setTimeout(() => setCopied(false), 2000);
28
+ } catch (err) {
29
+ console.error('Error al copiar:', err);
30
+ }
31
+ }
32
+ };
33
+
34
+ const charCount = value ? value.length : 0;
35
+
36
+ return (
37
+ <Field.Root
38
+ name={name}
39
+ id={name}
40
+ error={error}
41
+ hint={description && formatMessage(description)}
42
+ required={required}
43
+ >
44
+ <Flex justifyContent="space-between" alignItems="center">
45
+ <Field.Label action={labelAction}>
46
+ {formatMessage(intlLabel)} 🔒
47
+ </Field.Label>
48
+ <Flex gap={2}>
49
+ <IconButton
50
+ label={showValue ? 'Ocultar valor' : 'Mostrar valor'}
51
+ icon={showValue ? <EyeStriked /> : <Eye />}
52
+ onClick={() => setShowValue(!showValue)}
53
+ variant="ghost"
54
+ disabled={!value}
55
+ />
56
+ <IconButton
57
+ label={copied ? 'Copiado' : 'Copiar valor'}
58
+ icon={copied ? <Check /> : <Duplicate />}
59
+ onClick={handleCopy}
60
+ variant="ghost"
61
+ disabled={!value}
62
+ />
63
+ </Flex>
64
+ </Flex>
65
+ <Field.Input
66
+ type={showValue ? 'text' : 'password'}
67
+ placeholder={formatMessage({
68
+ id: 'encrypted-field.placeholder',
69
+ defaultMessage: 'Ingresa el texto a cifrar...',
70
+ })}
71
+ value={value || ''}
72
+ onChange={(e) => {
73
+ onChange({
74
+ target: { name, value: e.target.value, type: attribute.type },
75
+ });
76
+ }}
77
+ disabled={disabled}
78
+ />
79
+ <Flex justifyContent="space-between" alignItems="center" paddingTop={1}>
80
+ <Field.Hint />
81
+ {charCount > 0 && (
82
+ <Typography variant="pi" textColor="neutral600">
83
+ {charCount} {charCount === 1 ? 'carácter' : 'caracteres'}
84
+ </Typography>
85
+ )}
86
+ </Flex>
87
+ <Field.Error />
88
+ </Field.Root>
89
+ );
90
+ };
91
+
92
+ export default Input;
@@ -0,0 +1,103 @@
1
+ import Lock from '@strapi/icons/Lock';
2
+
3
+ export default {
4
+ register(app) {
5
+ app.customFields.register({
6
+ name: 'encrypted-text',
7
+ pluginId: 'encrypted-field',
8
+ type: 'text',
9
+ intlLabel: {
10
+ id: 'encrypted-field.label',
11
+ defaultMessage: 'Texto Cifrado',
12
+ },
13
+ intlDescription: {
14
+ id: 'encrypted-field.description',
15
+ defaultMessage: 'Campo de texto que se cifra automáticamente con AES-256-GCM',
16
+ },
17
+ icon: Lock,
18
+ components: {
19
+ Input: async () => import('./components/Input.jsx'),
20
+ },
21
+ options: {
22
+ base: [
23
+ {
24
+ sectionTitle: {
25
+ id: 'encrypted-field.options.advanced.settings',
26
+ defaultMessage: 'Configuración',
27
+ },
28
+ items: [
29
+ {
30
+ name: 'required',
31
+ type: 'checkbox',
32
+ intlLabel: {
33
+ id: 'encrypted-field.options.required.label',
34
+ defaultMessage: 'Campo requerido',
35
+ },
36
+ description: {
37
+ id: 'encrypted-field.options.required.description',
38
+ defaultMessage: 'No se podrá guardar sin este campo',
39
+ },
40
+ },
41
+ {
42
+ name: 'private',
43
+ type: 'checkbox',
44
+ intlLabel: {
45
+ id: 'encrypted-field.options.private.label',
46
+ defaultMessage: 'Campo privado',
47
+ },
48
+ description: {
49
+ id: 'encrypted-field.options.private.description',
50
+ defaultMessage: 'Este campo no será devuelto por la API',
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ advanced: [
57
+ {
58
+ sectionTitle: {
59
+ id: 'encrypted-field.options.advanced.regex',
60
+ defaultMessage: 'Validación',
61
+ },
62
+ items: [
63
+ {
64
+ name: 'regex',
65
+ type: 'text',
66
+ intlLabel: {
67
+ id: 'encrypted-field.options.regex.label',
68
+ defaultMessage: 'RegEx pattern',
69
+ },
70
+ description: {
71
+ id: 'encrypted-field.options.regex.description',
72
+ defaultMessage: 'Patrón de validación antes de cifrar',
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ ],
78
+ },
79
+ });
80
+ },
81
+
82
+ async registerTrads({ locales }) {
83
+ const importedTrads = await Promise.all(
84
+ locales.map((locale) => {
85
+ return import(`./translations/${locale}.json`)
86
+ .then(({ default: data }) => {
87
+ return {
88
+ data,
89
+ locale,
90
+ };
91
+ })
92
+ .catch(() => {
93
+ return {
94
+ data: {},
95
+ locale,
96
+ };
97
+ });
98
+ })
99
+ );
100
+
101
+ return Promise.resolve(importedTrads);
102
+ },
103
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "encrypted-field.label": "Encrypted Text",
3
+ "encrypted-field.description": "Text field that is automatically encrypted with AES-256-GCM",
4
+ "encrypted-field.placeholder": "Enter text to encrypt...",
5
+ "encrypted-field.options.advanced.settings": "Settings",
6
+ "encrypted-field.options.required.label": "Required field",
7
+ "encrypted-field.options.required.description": "You won't be able to save without this field",
8
+ "encrypted-field.options.private.label": "Private field",
9
+ "encrypted-field.options.private.description": "This field will not be returned by the API",
10
+ "encrypted-field.options.advanced.regex": "Validation",
11
+ "encrypted-field.options.regex.label": "RegEx pattern",
12
+ "encrypted-field.options.regex.description": "Validation pattern before encryption"
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "encrypted-field.label": "Texto Cifrado",
3
+ "encrypted-field.description": "Campo de texto que se cifra automáticamente con AES-256-GCM",
4
+ "encrypted-field.placeholder": "Ingresa el texto a cifrar...",
5
+ "encrypted-field.options.advanced.settings": "Configuración",
6
+ "encrypted-field.options.required.label": "Campo requerido",
7
+ "encrypted-field.options.required.description": "No se podrá guardar sin este campo",
8
+ "encrypted-field.options.private.label": "Campo privado",
9
+ "encrypted-field.options.private.description": "Este campo no será devuelto por la API",
10
+ "encrypted-field.options.advanced.regex": "Validación",
11
+ "encrypted-field.options.regex.label": "RegEx pattern",
12
+ "encrypted-field.options.regex.description": "Patrón de validación antes de cifrar"
13
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@growy/strapi-plugin-encrypted-field",
3
+ "version": "1.0.0",
4
+ "description": "Campo personalizado de texto cifrado para Strapi",
5
+ "strapi": {
6
+ "name": "encrypted-field",
7
+ "displayName": "Encrypted Field",
8
+ "description": "Agrega un campo de texto cifrado con AES-256-GCM",
9
+ "kind": "plugin"
10
+ },
11
+ "dependencies": {},
12
+ "peerDependencies": {
13
+ "@strapi/strapi": "^5.0.0"
14
+ },
15
+ "main": "server/index.js",
16
+ "author": {
17
+ "name": "Growy AI",
18
+ "email": "support@growy.ai",
19
+ "url": "https://getgrowy.ai"
20
+ },
21
+ "contributors": [
22
+ {
23
+ "name": "Zahir El isaac"
24
+ }
25
+ ],
26
+ "engines": {
27
+ "node": ">=18.0.0 <=22.x.x",
28
+ "npm": ">=6.0.0"
29
+ },
30
+ "license": "MIT",
31
+ "keywords": [
32
+ "strapi",
33
+ "strapi-plugin",
34
+ "encryption",
35
+ "aes-256-gcm",
36
+ "security",
37
+ "custom-field",
38
+ "encrypted-field",
39
+ "growy-ai",
40
+ "data-protection",
41
+ "cryptography",
42
+ "strapi-v5"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
@@ -0,0 +1,81 @@
1
+ const { encrypt, decrypt, validateValue, isEncryptedField } = require('./utils/crypto');
2
+
3
+ module.exports = ({ strapi }) => {
4
+ strapi.db.lifecycles.subscribe({
5
+ async beforeCreate(event) {
6
+ const { data } = event.params;
7
+ const model = strapi.getModel(event.model.uid);
8
+
9
+ for (const [key, attribute] of Object.entries(model.attributes)) {
10
+ if (!isEncryptedField(attribute)) continue;
11
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
12
+ const value = data[key];
13
+ if (value === null || value === undefined) continue;
14
+
15
+ const validation = validateValue(value, attribute);
16
+ if (!validation.valid) {
17
+ throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
18
+ }
19
+
20
+ data[key] = encrypt(value, strapi);
21
+ }
22
+ }
23
+ },
24
+
25
+ async beforeUpdate(event) {
26
+ const { data } = event.params;
27
+ const model = strapi.getModel(event.model.uid);
28
+
29
+ for (const [key, attribute] of Object.entries(model.attributes)) {
30
+ if (!isEncryptedField(attribute)) continue;
31
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
32
+ const value = data[key];
33
+ if (value === null || value === undefined) continue;
34
+
35
+ const validation = validateValue(value, attribute);
36
+ if (!validation.valid) {
37
+ throw new Error(`Validación fallida para el campo "${key}": ${validation.error}`);
38
+ }
39
+
40
+ data[key] = encrypt(value, strapi);
41
+ }
42
+ }
43
+ },
44
+
45
+ async afterFindOne(event) {
46
+ const { result } = event;
47
+ if (!result) return;
48
+
49
+ const model = strapi.getModel(event.model.uid);
50
+
51
+ for (const [key, attribute] of Object.entries(model.attributes)) {
52
+ if (!isEncryptedField(attribute)) continue;
53
+ if (Object.prototype.hasOwnProperty.call(result, key)) {
54
+ const value = result[key];
55
+ if (typeof value === 'string') {
56
+ result[key] = decrypt(value, strapi);
57
+ }
58
+ }
59
+ }
60
+ },
61
+
62
+ async afterFindMany(event) {
63
+ const { result } = event;
64
+ if (!result || !Array.isArray(result)) return;
65
+
66
+ const model = strapi.getModel(event.model.uid);
67
+
68
+ for (const item of result) {
69
+ for (const [key, attribute] of Object.entries(model.attributes)) {
70
+ if (!isEncryptedField(attribute)) continue;
71
+ if (Object.prototype.hasOwnProperty.call(item, key)) {
72
+ const value = item[key];
73
+ if (typeof value === 'string') {
74
+ item[key] = decrypt(value, strapi);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ },
80
+ });
81
+ };
@@ -0,0 +1,7 @@
1
+ const register = require('./register');
2
+ const bootstrap = require('./bootstrap');
3
+
4
+ module.exports = {
5
+ register,
6
+ bootstrap,
7
+ };
@@ -0,0 +1,11 @@
1
+ module.exports = ({ strapi }) => {
2
+ strapi.customFields.register({
3
+ name: 'encrypted-text',
4
+ plugin: 'encrypted-field',
5
+ type: 'text',
6
+ inputSize: {
7
+ default: 6,
8
+ isResizable: true,
9
+ },
10
+ });
11
+ };
@@ -0,0 +1,144 @@
1
+ const crypto = require('crypto');
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 12;
5
+ const AUTH_TAG_LENGTH = 16;
6
+ const KEY_LENGTH = 32;
7
+
8
+ function getEncryptionKey(strapi) {
9
+ const key = process.env.ENCRYPTION_KEY || strapi?.config?.get('plugin.encrypted-field.encryptionKey');
10
+
11
+ if (!key) {
12
+ 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
+ if (strapi?.log?.error) {
14
+ strapi.log.error(errorMsg);
15
+ }
16
+ throw new Error(errorMsg);
17
+ }
18
+
19
+ if (typeof key !== 'string' || key.length !== 64) {
20
+ throw new Error(`ENCRYPTION_KEY debe tener exactamente 64 caracteres hexadecimales (32 bytes). Actual: ${key?.length || 0}`);
21
+ }
22
+
23
+ if (!/^[0-9a-fA-F]{64}$/.test(key)) {
24
+ throw new Error('ENCRYPTION_KEY debe contener solo caracteres hexadecimales (0-9, a-f, A-F)');
25
+ }
26
+
27
+ return Buffer.from(key, 'hex');
28
+ }
29
+
30
+ function encrypt(text, strapi) {
31
+ if (typeof text !== 'string') return text;
32
+
33
+ if (text === '') return text;
34
+
35
+ try {
36
+ const ENCRYPTION_KEY = getEncryptionKey(strapi);
37
+ const iv = crypto.randomBytes(IV_LENGTH);
38
+ const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
39
+
40
+ let encrypted = cipher.update(text, 'utf8', 'hex');
41
+ encrypted += cipher.final('hex');
42
+
43
+ const authTag = cipher.getAuthTag();
44
+
45
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
46
+ } catch (error) {
47
+ if (strapi?.log?.error) {
48
+ strapi.log.error(`Error al cifrar: ${error.message}`);
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ function decrypt(encryptedText, strapi) {
55
+ if (!encryptedText || typeof encryptedText !== 'string') return encryptedText;
56
+
57
+ const parts = encryptedText.split(':');
58
+ if (parts.length !== 3) {
59
+ return encryptedText;
60
+ }
61
+
62
+ try {
63
+ const [ivHex, authTagHex, encrypted] = parts;
64
+
65
+ if (ivHex.length !== IV_LENGTH * 2 || authTagHex.length !== AUTH_TAG_LENGTH * 2) {
66
+ return encryptedText;
67
+ }
68
+
69
+ const ENCRYPTION_KEY = getEncryptionKey(strapi);
70
+ const iv = Buffer.from(ivHex, 'hex');
71
+ const authTag = Buffer.from(authTagHex, 'hex');
72
+
73
+ const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
74
+ decipher.setAuthTag(authTag);
75
+
76
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
77
+ decrypted += decipher.final('utf8');
78
+
79
+ return decrypted;
80
+ } catch (error) {
81
+ if (strapi?.log?.debug) {
82
+ strapi.log.debug(`Error al descifrar: ${error.message}. Retornando texto original.`);
83
+ }
84
+ return encryptedText;
85
+ }
86
+ }
87
+
88
+ function validateValue(value, attribute) {
89
+ if (value === null || value === undefined || value === '') {
90
+ return { valid: true };
91
+ }
92
+
93
+ if (typeof value !== 'string') {
94
+ return {
95
+ valid: false,
96
+ error: 'El valor debe ser una cadena de texto'
97
+ };
98
+ }
99
+
100
+ if (attribute.regex) {
101
+ try {
102
+ const regex = new RegExp(attribute.regex);
103
+ if (!regex.test(value)) {
104
+ return {
105
+ valid: false,
106
+ error: `El valor no cumple con el patrón de validación: ${attribute.regex}`
107
+ };
108
+ }
109
+ } catch (error) {
110
+ return {
111
+ valid: false,
112
+ error: `Patrón regex inválido: ${error.message}`
113
+ };
114
+ }
115
+ }
116
+
117
+ 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`
121
+ };
122
+ }
123
+
124
+ if (attribute.minLength && value.length < attribute.minLength) {
125
+ return {
126
+ valid: false,
127
+ error: `El valor debe tener al menos ${attribute.minLength} caracteres`
128
+ };
129
+ }
130
+
131
+ return { valid: true };
132
+ }
133
+
134
+ function isEncryptedField(attribute) {
135
+ return attribute?.customField === 'plugin::encrypted-field.encrypted-text';
136
+ }
137
+
138
+ module.exports = {
139
+ encrypt,
140
+ decrypt,
141
+ validateValue,
142
+ isEncryptedField,
143
+ getEncryptionKey,
144
+ };