Encrypt sensitive credential fields before event bus emission (#215)

- Add encrypt_credential_dict() and decrypt_credential_field() to utils/crypto.py
- Create utils/credential_encryption.py with encryption/decryption helpers
- Add get_credential_encryption_key() to retrieve key from Infisical at runtime
- Add emit_credential_found() wrapper for modules to use instead of direct bus.emit()
- Update all 15 modules emitting CREDENTIAL_FOUND to use encrypted wrapper
- Update 4 modules consuming CREDENTIAL_FOUND to decrypt payload before processing
- Sensitive fields (username, password, hash, token, etc.) encrypted with AES-256-GCM
- Falls back to plaintext if encryption key unavailable or encryption fails
This commit is contained in:
Cobra
2026-04-06 11:43:46 -04:00
parent f15b8994cd
commit edf2ea7c36
18 changed files with 256 additions and 63 deletions
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Helper to get encryption key from Infisical and encrypt credentials before emission."""
import subprocess
import json
import logging
from typing import Optional
logger = logging.getLogger("sensor.credential_encryption")
# Cache the key in memory during module lifetime
_cached_key: Optional[bytes] = None
def get_credential_encryption_key() -> Optional[bytes]:
"""Retrieve credential encryption key from Infisical via creds CLI.
Returns None if key cannot be retrieved (in which case plaintext fallback).
Caches key in memory for the lifetime of the module process.
"""
global _cached_key
if _cached_key is not None:
return _cached_key
try:
# Try to get CREDENTIAL_ENCRYPTION_KEY from Infisical via creds CLI
result = subprocess.run(
["/home/n0mad1k/.local/bin/creds", "get", "CREDENTIAL_ENCRYPTION_KEY"],
capture_output=True,
timeout=5,
text=True,
)
if result.returncode != 0:
logger.warning("Failed to retrieve CREDENTIAL_ENCRYPTION_KEY from Infisical: %s", result.stderr)
return None
key_hex = result.stdout.strip()
if not key_hex:
logger.warning("CREDENTIAL_ENCRYPTION_KEY is empty in Infisical")
return None
# Convert hex string to bytes
_cached_key = bytes.fromhex(key_hex)
if len(_cached_key) != 32:
logger.error("CREDENTIAL_ENCRYPTION_KEY must be 32 bytes (256-bit), got %d", len(_cached_key))
_cached_key = None
return None
logger.debug("Successfully loaded CREDENTIAL_ENCRYPTION_KEY from Infisical")
return _cached_key
except FileNotFoundError:
logger.error("creds CLI not found at ~/.local/bin/creds")
return None
except subprocess.TimeoutExpired:
logger.error("Timeout retrieving CREDENTIAL_ENCRYPTION_KEY from Infisical")
return None
except Exception as e:
logger.error("Error retrieving CREDENTIAL_ENCRYPTION_KEY: %s", e)
return None
def emit_credential_found(bus, source_module: str, payload: dict) -> None:
"""Emit CREDENTIAL_FOUND event with encrypted sensitive fields.
Sensitive fields are encrypted before emission. If encryption fails,
plaintext fallback is used with a warning logged.
Args:
bus: EventBus instance
source_module: Module name emitting the event
payload: Credential payload dict with fields like username, password, etc.
"""
from utils.crypto import encrypt_credential_dict
key = get_credential_encryption_key()
if key:
try:
encrypted_payload = encrypt_credential_dict(payload, key)
bus.emit("CREDENTIAL_FOUND", encrypted_payload, source_module=source_module)
return
except Exception as e:
logger.warning("Failed to encrypt credential payload: %s — using plaintext fallback", e)
# Fallback: emit plaintext with warning
bus.emit("CREDENTIAL_FOUND", payload, source_module=source_module)
def decrypt_credential_payload(payload: dict) -> dict:
"""Decrypt sensitive fields in credential event payload.
Looks for fields marked with __encrypted__ prefix and decrypts them.
Returns a new dict with decrypted fields.
Args:
payload: Credential dict with possibly encrypted fields
Returns:
New dict with decrypted values
"""
from utils.crypto import decrypt_credential_field
key = get_credential_encryption_key()
if not key:
return payload
decrypted = {}
for field, value in payload.items():
if isinstance(value, str) and value.startswith("__encrypted__"):
try:
decrypted[field] = decrypt_credential_field(value, key)
except Exception as e:
logger.warning("Failed to decrypt field %s: %s", field, e)
decrypted[field] = value
else:
decrypted[field] = value
return decrypted
+59
View File
@@ -73,6 +73,65 @@ class CryptoEngine:
return self._aesgcm.decrypt(nonce, ct, aad)
def encrypt_credential_dict(payload: dict, key: bytes) -> dict:
"""Encrypt sensitive fields in credential event payload before bus emission.
Sensitive fields: username, password, hash, domain, token, secret, api_key
Returns a new dict with encrypted fields as bytes wrapped in __encrypted__ markers.
"""
if len(key) != KEY_SIZE:
raise ValueError(f"Key must be {KEY_SIZE} bytes")
engine = CryptoEngine(key)
encrypted = {}
sensitive_fields = {
"username", "password", "hash", "hash_value", "domain",
"token", "secret", "api_key", "access_token", "refresh_token",
"credential_value", "nt_hash", "lm_hash", "response",
"auth_string", "bearer_token"
}
for field, value in payload.items():
if field.lower() in sensitive_fields and isinstance(value, str) and value:
try:
# Encrypt the string value
plaintext = value.encode("utf-8")
ciphertext = engine.encrypt(plaintext)
# Encode to base64 for transport over JSON
import base64
encrypted[field] = f"__encrypted__{base64.b64encode(ciphertext).decode('ascii')}"
except Exception:
# If encryption fails, include plaintext (log warning in caller)
encrypted[field] = value
else:
# Pass through unencrypted
encrypted[field] = value
return encrypted
def decrypt_credential_field(value: str, key: bytes) -> str:
"""Decrypt a single encrypted credential field."""
if not isinstance(value, str) or not value.startswith("__encrypted__"):
return value
if len(key) != KEY_SIZE:
raise ValueError(f"Key must be {KEY_SIZE} bytes")
try:
import base64
engine = CryptoEngine(key)
encrypted_part = value[len("__encrypted__"):]
ciphertext = base64.b64decode(encrypted_part)
plaintext = engine.decrypt(ciphertext)
return plaintext.decode("utf-8")
except Exception:
# Return original if decryption fails
return value
def encrypt_file(
src: str,
dst: str,