CreaRack-SL

Servicio crypto del Agente — Cifrado DPAPI en reposo (terminal/agent/core/crypto.py)

Ubicación

Ruta: terminal/agent/core/crypto.py
Módulo Python: terminal.agent.core.crypto
Líneas: ~103
Estado: ✅ Activo desde s124 (2026-06-10)

Propósito

Encripta secretos sensibles (credenciales SNMP: snmp_community, snmp_v3_auth_key, snmp_v3_priv_key) antes de escribirlos en la BD local (metrics.db) del Agent. Usa Windows DPAPI (Data Protection API), atado a la cuenta Windows del usuario, garantizando que solo esa cuenta puede descifrar los datos.

Interfaz pública

Función encrypt_secret(value: str | None) -> str | None

Firma:

def encrypt_secret(value: str | None) -> str | None

Propósito: Encripta un secreto para almacenamiento en reposo.

Comportamiento:

  • Valores None/vacíos: pasan sin cambios.
  • Valores ya encriptados (prefijo "dpapi:"): retorna sin cambios (idempotente).
  • No-Windows (dev/CI): retorna el valor tal cual (la producción solo corre en Windows .exe).
  • En error: logea excepción y retorna el valor en claro (fallback seguro — nunca falla).

Retorno: string con prefijo "dpapi:" + token base64, o el valor original si no se cifró.

Función decrypt_secret(value: str | None) -> str | None

Firma:

def decrypt_secret(value: str | None) -> str | None

Propósito: Descifra un secreto almacenado.

Comportamiento:

  • Valores None/vacíos: pasan sin cambios.
  • Valores en claro (sin prefijo "dpapi:"): pasan sin cambios (compatibilidad con DBs legacy previas al cifrado).
  • Valores encriptados: descifra usando Windows DPAPI.
  • En error: logea excepción y retorna string vacío.

Retorno: el secreto en claro, o string vacío si descifrado falló.

Funciones internas (Windows DPAPI)

_dpapi_encrypt(data: bytes) -> bytes

Llama a CryptProtectData (ctypes). Eleva RuntimeError si falla. Libera memoria alocada por Windows.

_dpapi_decrypt(data: bytes) -> bytes

Llama a CryptUnprotectData (ctypes). Eleva RuntimeError si falla. Libera memoria.

Integración en el sistema

core/store.py — TimeSeriesStore

En lectura (get_target_list()):

for row in rows:
    for field in _SECRET_TARGET_FIELDS:  # ("snmp_community", "snmp_v3_auth_key", "snmp_v3_priv_key")
        if field in row:
            row[field] = decrypt_secret(row[field])
return rows

En escritura (upsert_target()):

encrypt_secret(target.get("snmp_community")),
encrypt_secret(target.get("snmp_v3_auth_key", "")),
encrypt_secret(target.get("snmp_v3_priv_key", "")),

Flujo completo

  1. SaaS → Agent (vía WebSocket): SaaS envía target con credenciales en claro.
  2. Agent escribe (upsert_target): crypto.encrypt_secret() → almacena con prefijo "dpapi:" en metrics.db.
  3. Agent lee (get_target_list): crypto.decrypt_secret() → descifra en memoria, retorna en claro a quien lo llamó (ej. routes/network.py).
  4. Uso local: credencial está en claro solo en memoria del proceso Agent, nunca toca disco así.

Compatibilidad hacia atrás (legacy)

Problema: DBs existentes pueden tener credenciales en claro (s123 y antes).

Solución: decrypt_secret() detecta el prefijo "dpapi:":

  • Con prefijo → descifra (valor nuevo).
  • Sin prefijo → retorna como-está (valor legacy en claro).

Auto-recifrado: cuando SaaS hace upsert_target() (push de actualización), encrypt_secret() recifra el valor, migrando la BD poco a poco sin downtime.

Detalles de implementación

Formato de almacenamiento

"dpapi:" + base64(CryptProtectData(utf-8_bytes))
  • Prefijo: "dpapi:" (ASCII, 6 caracteres).
  • Token: base64 del binario cifrado por DPAPI.
  • Ventaja: distingue valores cifrados de legacy sin necesidad de metadatos extra.

Variables privadas

  • _ENC_PREFIX = "dpapi:" — prefijo de detección.
  • _WINDOWS = sys.platform == "win32" — flag para evitar importar ctypes en non-Windows.
  • _SECRET_TARGET_FIELDS en store.py — tupla de nombres de columna a cifrar/descifrar.

Logging

Usa logger = logging.getLogger("agent.crypto") → mensajes en nivel INFO/ERROR, sin exponer valores secretos.

Seguridad

DPAPI (Data Protection API)

  • Estándar Windows: encriptación a nivel SO, tied a user account + machine key.
  • No requiere master key: el usuario no necesita recordar/guardar una contraseña.
  • Limitación: solo funciona en Windows. En dev/non-Windows, los valores se almacenan en claro (es una trade-off aceptable: la producción es solo Windows .exe).

Protección del known_hosts

El fichero known_hosts (creado por network/host_keys.py) contiene public keys de los dispositivos, no secretos. No requiere cifrado DPAPI (los public keys no son confidenciales).

Testing

  • tests/agent/test_agent_hardening_b1.py incluye tests para encrypt_secret / decrypt_secret.
  • Prueba de round-trip DPAPI se salta fuera de Windows (la fixture verifica sys.platform).
  • Pruebas de compatibilidad legacy (valor en claro + descifrado).

Notas de operación

  • Sin migrations: crypto.py es un módulo puro de utilidad, sin modelos Django.
  • No bloquea: encriptación DPAPI es O(1) en la mayoría de casos (claves cortas <1KB).
  • Error handling: si DPAPI falla (ej. user account corrupted), la función loga y retorna un string seguro (vacío en descifrado, claro en encriptado) — nunca levanta excepción no-manejada.

Véase también

  • [[feature—terminal—agent-hardening-b1]]
  • [[entity—terminal—service—agent-host-keys]]
  • [[entity—terminal—service—store]]
  • [[concept—security—dpapi-windows]]
  • [[concept—security—at-rest-encryption]]