feat(backend): c2 crypto + config CRUD + adapter scaffolding (sprint 8 M1)
- Add Fernet crypto service (MIMIC_ENCRYPTION_KEY env, C2Disabled on absent key) - Add Alembic migration 0006: c2_config + c2_task tables with cascade FKs - Add C2Config and C2Task SQLAlchemy models - Add C2Adapter ABC with dataclasses (C2Health, C2Callback, C2TaskStatus, C2TaskPage) - Add FakeAdapter (deterministic in-memory, MIMIC_C2_ADAPTER=fake) - Add MythicAdapter scaffold: test_connection() live, M2+ raise NotImplementedError - Add decode_response_text() helper for base64/binary Mythic responses - Add GET/PUT/DELETE/POST-test /api/engagements/<id>/c2-config endpoints - RBAC: admin+redteam OK, SOC 403; 503 guard when encryption key absent - Token never returned in API responses; stored Fernet-encrypted only - 42 new tests (300 total, 258 baseline preserved green) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
40
backend/app/services/crypto.py
Normal file
40
backend/app/services/crypto.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Fernet-based encryption service for sensitive fields.
|
||||
|
||||
Key is read from the MIMIC_ENCRYPTION_KEY env var (Fernet base64-urlsafe 32-byte key).
|
||||
When the key is absent the service raises C2Disabled so callers can return 503.
|
||||
The key is never logged or returned in any response.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
|
||||
class C2Disabled(Exception):
|
||||
"""Raised when MIMIC_ENCRYPTION_KEY is not set."""
|
||||
|
||||
|
||||
def _get_fernet() -> Fernet:
|
||||
key = os.environ.get("MIMIC_ENCRYPTION_KEY")
|
||||
if not key:
|
||||
raise C2Disabled("C2 disabled: MIMIC_ENCRYPTION_KEY not set")
|
||||
return Fernet(key.encode() if isinstance(key, str) else key)
|
||||
|
||||
|
||||
def encrypt(plaintext: str) -> str:
|
||||
"""Encrypt *plaintext* and return a Fernet token (str)."""
|
||||
f = _get_fernet()
|
||||
return f.encrypt(plaintext.encode()).decode()
|
||||
|
||||
|
||||
def decrypt(ciphertext: str) -> str:
|
||||
"""Decrypt a Fernet token and return the plaintext string."""
|
||||
f = _get_fernet()
|
||||
try:
|
||||
return f.decrypt(ciphertext.encode()).decode()
|
||||
except InvalidToken as exc:
|
||||
raise ValueError("Invalid ciphertext") from exc
|
||||
|
||||
|
||||
__all__ = ["C2Disabled", "encrypt", "decrypt"]
|
||||
Reference in New Issue
Block a user