Skip to content

Cifrar secretos con la clave de la instalación (AppKey) - #2058

Merged
NeoRazorX merged 2 commits into
NeoRazorX:masterfrom
daniel89fg:appkey-cifrado
Oct 28, 2026
Merged

NeoRazorX merged 2 commits into
NeoRazorX:masterfrom
daniel89fg:appkey-cifrado

Conversation

@daniel89fg

@daniel89fg daniel89fg commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Descripción

AppKey firma los tokens con FS_APP_KEY, pero no había una forma común de cifrar los secretos que hay que guardar: contraseñas de certificados, de buzones de correo, de servicios externos, etc. Cada plugin los guarda en claro o con su propio esquema. Esta PR añade a AppKey el par que faltaba para cifrar y descifrar, con el mismo enfoque que sign()/verify(): un propósito que separa cada uso.

No cambia nada de lo que ya existe: son tres métodos nuevos y sus tests.

Cambios realizados

  • AppKey::encrypt(string $purpose, string $plain, string $context = ''): ?string: AES-256-GCM con una subclave de FS_APP_KEY derivada con HKDF-SHA256 y el propósito (nunca con FS_APP_KEY tal cual, que firma los tokens). El contexto se autentica sin cifrarlo: por ejemplo, el id de la fila, para que el valor cifrado no sirva si se copia a otra fila. Formato: v1: + base64(IV de 12 bytes ‖ etiqueta de 16 ‖ texto cifrado). El prefijo permite cambiar el formato en el futuro sin perder lo guardado.
  • AppKey::decrypt(string $purpose, ?string $value, string $context = ''): ?string: devuelve null si el valor no tiene el formato, se ha modificado, ha cambiado FS_APP_KEY o el propósito o el contexto no coinciden.
  • AppKey::canEncrypt(): bool: sin FS_APP_KEY en el config.php no se cifra (encrypt() devuelve null), porque la clave derivada cambia al cambiar los datos de conexión y se perdería lo guardado. Quien lo use puede mostrar el aviso app-key-missing que ya tiene el core.
  • Tests en AppKeyTest: sin clave no cifra; ida y vuelta (también con un secreto vacío y con caracteres especiales); IV aleatorio; otro propósito, otro contexto, valor modificado, sin prefijo, corto, vacío o null; y un test que fija el formato (testEncryptFormat).

Uso típico:

$cifrado = AppKey::encrypt('MiPlugin|smtp', $password, (string)$empresa->idempresa);
$password = AppKey::decrypt('MiPlugin|smtp', $cifrado, (string)$empresa->idempresa);

Ya lo usan, con este mismo formato, los plugins fiscales españoles (contraseñas de los certificados de empresa y del buzón IMAP de Facturae), que hoy llevan su propia copia. Con el mismo propósito, lo que ya han cifrado se descifra con AppKey::decrypt(). Más adelante, el core podría cifrar con esto la contraseña del correo (ConfigEmail), que hoy se guarda en claro; eso sería otra PR.

  • Test intermitente de PanelControllerTest (commit aparte): testWidgetSubcuentaSearchShowsSelectedFirst buscaba por el código de la cuenta de prueba (9999) en el código y la descripción de las subcuentas del ejercicio. Los clientes aleatorios de otros tests ("Empresa 19999") dejan subcuentas cuya descripción puede contenerlo, y el test fallaba con una de más (así falló la primera ejecución de la CI de esta PR, con 4300000010). Ahora busca por una descripción única de sus subcuentas.

Cómo probar

  1. Con FS_APP_KEY en el config.php: AppKey::decrypt('test', AppKey::encrypt('test', 'secreto', '1'), '1') devuelve secreto; con otro propósito o con otro contexto devuelve null.
  2. Sin FS_APP_KEY: AppKey::canEncrypt() es false y AppKey::encrypt() devuelve null.
  3. Tests: AppKeyTest (testCanEncrypt, testEncryptAndDecrypt, testEncryptFormat). La batería completa del core pasa en PHP 8.1–8.5 con MySQL y PostgreSQL.

AppKey firma tokens con FS_APP_KEY, pero no había una forma común de
cifrar los secretos que hay que guardar (contraseñas de certificados,
de buzones de correo, etc.) y cada plugin los guardaba en claro o con
su propio esquema.

- AppKey::encrypt() y decrypt(): AES-256-GCM con una subclave de
  FS_APP_KEY derivada con HKDF-SHA256 y el propósito, y un contexto
  autenticado (por ejemplo, el id de la fila) para que el valor no sirva
  en otra fila. Formato "v1:" + base64(IV, etiqueta y texto cifrado).
- AppKey::canEncrypt(): sin FS_APP_KEY no se cifra, porque la clave
  derivada cambia con los datos de conexión y se perdería lo guardado.
- Tests de ida y vuelta, propósito, contexto, valores manipulados y
  formato.
@elguitarraverde

Copy link
Copy Markdown
Contributor

@daniel89fg graciasss!!! porfin llegó!!! hacia falta desde hace tiempo

hice esta pr por si te sirve, pero vamos que creo que es lo mismo que has hecho.
#1548

es que en aquella epoca(del PR) necesitaba guardar unas keys de un servicio y claro las guarda en texto plano o se pueden hacer trucos, pero esto es mejor.

para dejarlo seguro del todo se debería poder configurar FS_APP_KEY en las variables de entorno que ahí ya no puede acceder nadie aunque entren en el servidor.

testWidgetSubcuentaSearchShowsSelectedFirst buscaba por el código de la
cuenta de prueba (9999) en el código y la descripción de las subcuentas
del ejercicio. Los clientes aleatorios de otros tests ("Empresa 19999")
dejan subcuentas cuya descripción puede contenerlo, y entonces el test
fallaba con una subcuenta de más (por ejemplo, 4300000010).

@NeoRazorX NeoRazorX left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisado: correcto. phpcs limpio y AppKeyTest, MyFilesTokenTest y PanelControllerTest pasan en local con PHP 8.3 + MariaDB (sin FS_APP_KEY), PHP 8.1 + MySQL y PHP 8.4 + PostgreSQL. Añado después en master que canEncrypt() compruebe que está la extensión openssl.

@NeoRazorX
NeoRazorX merged commit d2cbd93 into NeoRazorX:master Oct 28, 2026
23 checks passed
@daniel89fg
daniel89fg deleted the appkey-cifrado branch October 29, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants