#!/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(__name__) # 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: import os creds_bin = os.environ.get("BB_CREDS_BIN") if not creds_bin: raise FileNotFoundError( "BB_CREDS_BIN environment variable not set. " "Set it to the path of your Infisical creds CLI binary, e.g. " "export BB_CREDS_BIN=/usr/local/bin/creds" ) # Try to get CREDENTIAL_ENCRYPTION_KEY from Infisical via creds CLI result = subprocess.run( [creds_bin, "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