services/api/src/utils/encryption.js

Overview

Este archivo define utilidades criptográficas para proteger datos sensibles en el backend. Su función principal es cifrar, descifrar y derivar identificadores deterministas a partir de valores como retentionContact, email o telefono.

El módulo trabaja con env.retentionEncryptionKey desde services/api/src/config/env.js y lanza ApiError desde services/api/src/utils/apiError.js cuando detecta formatos inválidos o fallos de descifrado. Usa crypto de Node.js como base criptográfica.

[!IMPORTANT] encryptField y decryptField tratan las entradas vacías como caso especial y devuelven '' sin lanzar error.

How it works

flowchart TB
    EnvKey[env retentionEncryptionKey] --> GetKey[getKey]
    GetKey --> Encrypt[encryptField]
    GetKey --> Decrypt[decryptField]
    GetKey --> Digest[derivePurgedIdentifierDigest]

    Encrypt --> Iv[Random IV]
    Encrypt --> Tag[Auth tag]
    Encrypt --> Ciphertext[Ciphertext hex]
    Iv --> Packed[Packed value]
    Tag --> Packed
    Ciphertext --> Packed

    Decrypt --> Parts[Split packed value]
    Parts -->|3 parts| Valid[Decrypt and validate]
    Parts -->|invalid format| FormatError[ApiError 500]
    Valid --> Plaintext[Plaintext utf8]
    Valid -->|auth fail| DecryptError[ApiError 500]

    Digest --> Hmac[HMAC SHA 256]
    Hmac --> HexDigest[Hex digest]

El flujo parte de una clave hexagonal definida en env.retentionEncryptionKey. Esa clave se convierte en un Buffer y se usa tanto para cifrado AES-256-GCM como para HMAC-SHA256. encryptField empaqueta el resultado en un formato de tres segmentos separados por :. decryptField espera exactamente ese formato y valida la autenticidad antes de devolver texto plano.

Key components

{
    "title": "getKey",
    "content": "Funci\u00f3n interna que convierte `env.retentionEncryptionKey` desde hexadecimal a `Buffer`. Sirve como fuente com\u00fan de clave para cifrado, descifrado y digest determinista.",
    "type": "function",
    "filePath": "services/api/src/utils/encryption.js",
    "badges": []
}
{
    "title": "encryptField",
    "content": "Funci\u00f3n p\u00fablica que cifra un valor sensible con `aes-256-gcm`. Devuelve una cadena en formato `ivHex:authTagHex:ciphertextHex` o `''` si la entrada est\u00e1 vac\u00eda.",
    "type": "function",
    "filePath": "services/api/src/utils/encryption.js",
    "badges": [
        "public"
    ]
}
{
    "title": "decryptField",
    "content": "Funci\u00f3n p\u00fablica que invierte el valor producido por `encryptField`. Devuelve `''` para entradas vac\u00edas y lanza `ApiError` si el formato es inv\u00e1lido o la autenticidad falla.",
    "type": "function",
    "filePath": "services/api/src/utils/encryption.js",
    "badges": [
        "public"
    ]
}
{
    "title": "derivePurgedIdentifierDigest",
    "content": "Funci\u00f3n p\u00fablica que genera un digest determinista de un solo sentido con HMAC-SHA256. Se usa para reemplazar identificadores como `email` y `telefono` durante la purga definitiva.",
    "type": "function",
    "filePath": "services/api/src/utils/encryption.js",
    "badges": [
        "public"
    ]
}

Data & fields

Este módulo no define clases ni estructuras persistentes. Su estado relevante se reduce a constantes internas y al formato de los valores cifrados.

[
    {
        "name": "env.retentionEncryptionKey",
        "type": "string",
        "required": true,
        "description": "Clave criptográfica en hexadecimal. `getKey()` la transforma en `Buffer` y la reutiliza para cifrado, descifrado y HMAC."
    },
    {
        "name": "ALGORITHM",
        "type": "string",
        "required": true,
        "description": "Algoritmo fijo definido como `aes-256-gcm`."
    },
    {
        "name": "IV_LENGTH",
        "type": "number",
        "required": true,
        "description": "Longitud fija del vector de inicialización. El valor es `12` bytes."
    }
]

encryptField produce una cadena con tres partes separadas por dos puntos.

[
    {
        "name": "packedValue",
        "type": "string",
        "required": true,
        "description": "Cadena devuelta por `encryptField`."
    },
    {
        "name": "ivHex",
        "type": "string",
        "required": true,
        "description": "Vector de inicialización codificado en hexadecimal."
    },
    {
        "name": "authTagHex",
        "type": "string",
        "required": true,
        "description": "Etiqueta de autenticación generada por GCM y codificada en hexadecimal."
    },
    {
        "name": "ciphertextHex",
        "type": "string",
        "required": true,
        "description": "Texto cifrado codificado en hexadecimal."
    }
]

El orden es importante. decryptField divide el valor por : y exige exactamente tres segmentos.

derivePurgedIdentifierDigest(userId) usa userId como entrada y devuelve un digest hexadecimal. El comentario del archivo indica que este valor se usa en la purga definitiva del día 60 para sustituir email y telefono.

[
    {
        "name": "userId",
        "type": "string",
        "required": true,
        "description": "Identificador del usuario que alimenta el HMAC."
    },
    {
        "name": "digest",
        "type": "string",
        "required": true,
        "description": "Salida hexadecimal del HMAC-SHA256."
    }
]

Behaviors

  • getKey — Convierte env.retentionEncryptionKey desde hexadecimal a Buffer y la devuelve.
  • encryptField — Cifra un valor con aes-256-gcm y devuelve el paquete hexadecimal.
  • decryptField — Valida y descifra el paquete generado por encryptField, o lanza ApiError.
  • derivePurgedIdentifierDigest — Genera un digest HMAC-SHA256 determinista a partir de userId.

How the encryption path behaves

encryptField crea un IV aleatorio de 12 bytes con crypto.randomBytes. Después construye un cipher con crypto.createCipheriv, cifra el texto en UTF-8 y obtiene la auth tag. El resultado final concatena iv, authTag y ciphertext, todos en hexadecimal.

decryptField primero valida el formato. Si la cadena no tiene tres partes, lanza ApiError con el mensaje retentionContact tiene un formato invalido. Si el formato es correcto, reconstruye el decipher, aplica la auth tag y devuelve el texto plano. Si algo falla, lanza ApiError con code: 'DECRYPTION_FAILED'.

derivePurgedIdentifierDigest no cifra datos. En su lugar, genera un digest de un solo sentido con crypto.createHmac('sha256', getKey()). Esto produce una salida determinista para el mismo userId.

[!NOTE] El mismo secreto env.retentionEncryptionKey alimenta tanto el cifrado AES como el HMAC.

Gotchas & edge cases

[!WARNING] decryptField no devuelve un valor parcial ni silencioso cuando el paquete está manipulado. Si la auth tag no valida, lanza ApiError.

[!WARNING] Si el valor de entrada es vacío, encryptField y decryptField devuelven '' sin error. Ese comportamiento permite tratar el campo como opcional.

[!CAUTION] decryptField exige exactamente tres segmentos separados por :. Cualquier otro formato produce un error explícito.

[!IMPORTANT] El archivo asume que env.retentionEncryptionKey ya está disponible y en formato hexadecimal. getKey() no hace validación adicional en este módulo.

Este archivo se integra con componentes concretos del backend y no introduce dependencias externas adicionales.

  • services/api/src/config/env.js — Proporciona env.retentionEncryptionKey.
  • services/api/src/utils/apiError.js — Define ApiError, usado para errores de formato y descifrado.
  • crypto de Node.js — Aporta randomBytes, createCipheriv, createDecipheriv y createHmac.

[!NOTE] El archivo solo contiene utilidades criptográficas. No expone endpoints HTTP ni lógica de controladores.