""" Encryption Key Management API (Admin only). Provides endpoints for: - Listing encryption keys (without actual key data) - Creating new encryption keys - Key rotation - Checking encryption status """ import uuid from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session from typing import Optional from app.core.database import get_db from app.middleware.auth import get_current_user, require_system_admin from app.models import User, EncryptionKey, AuditAction from app.schemas.encryption_key import ( EncryptionKeyResponse, EncryptionKeyListResponse, EncryptionKeyCreateResponse, EncryptionKeyRotateResponse, EncryptionStatusResponse, ) from app.services.encryption_service import ( encryption_service, MasterKeyNotConfiguredError, ) from app.services.audit_service import AuditService router = APIRouter(prefix="/api/admin/encryption-keys", tags=["Admin - Encryption Keys"]) def key_to_response(key: EncryptionKey) -> EncryptionKeyResponse: """Convert EncryptionKey model to response (without key data).""" return EncryptionKeyResponse( id=key.id, algorithm=key.algorithm, is_active=key.is_active, created_at=key.created_at, rotated_at=key.rotated_at, ) @router.get("/status", response_model=EncryptionStatusResponse) async def get_encryption_status( db: Session = Depends(get_db), current_user: User = Depends(require_system_admin), ): """ Get the current encryption status. Returns whether encryption is available, active key info, and total key count. """ encryption_available = encryption_service.is_encryption_available() active_key = None total_keys = 0 if encryption_available: active_key = db.query(EncryptionKey).filter( EncryptionKey.is_active == True ).first() total_keys = db.query(EncryptionKey).count() message = "Encryption is available" if encryption_available else "Encryption is not configured (ENCRYPTION_MASTER_KEY not set)" return EncryptionStatusResponse( encryption_available=encryption_available, active_key_id=active_key.id if active_key else None, total_keys=total_keys, message=message, ) @router.get("", response_model=EncryptionKeyListResponse) async def list_encryption_keys( db: Session = Depends(get_db), current_user: User = Depends(require_system_admin), ): """ List all encryption keys (without actual key data). Only accessible by system administrators. """ keys = db.query(EncryptionKey).order_by(EncryptionKey.created_at.desc()).all() return EncryptionKeyListResponse( keys=[key_to_response(k) for k in keys], total=len(keys), ) @router.post("", response_model=EncryptionKeyCreateResponse) async def create_encryption_key( request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_system_admin), ): """ Create a new encryption key. The key is generated, encrypted with the Master Key, and stored. This does NOT automatically make it the active key - use rotate for that. """ if not encryption_service.is_encryption_available(): raise HTTPException( status_code=400, detail="Encryption is not configured. Set ENCRYPTION_MASTER_KEY in environment." ) try: # Generate new key raw_key = encryption_service.generate_key() # Encrypt with master key encrypted_key = encryption_service.encrypt_key(raw_key) # Create key record (not active by default) key = EncryptionKey( id=str(uuid.uuid4()), key_data=encrypted_key, algorithm="AES-256-GCM", is_active=False, ) db.add(key) # Audit log AuditService.log_event( db=db, event_type="encryption.key_create", resource_type="encryption_key", action=AuditAction.CREATE, user_id=current_user.id, resource_id=key.id, changes=[{"field": "algorithm", "old_value": None, "new_value": "AES-256-GCM"}], request_metadata=getattr(request.state, "audit_metadata", None), ) db.commit() db.refresh(key) return EncryptionKeyCreateResponse( id=key.id, algorithm=key.algorithm, is_active=key.is_active, created_at=key.created_at, message="Encryption key created successfully. Use /rotate to make it active.", ) except MasterKeyNotConfiguredError: raise HTTPException( status_code=400, detail="Master key is not configured" ) except Exception as e: db.rollback() raise HTTPException( status_code=500, detail=f"Failed to create encryption key: {str(e)}" ) @router.post("/rotate", response_model=EncryptionKeyRotateResponse) async def rotate_encryption_key( request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_system_admin), ): """ Rotate to a new encryption key. This will: 1. Create a new encryption key 2. Mark the new key as active 3. Mark the old active key as inactive (but keep it for decrypting old files) After rotation, new file uploads will use the new key. Old files remain readable using their original keys. """ if not encryption_service.is_encryption_available(): raise HTTPException( status_code=400, detail="Encryption is not configured. Set ENCRYPTION_MASTER_KEY in environment." ) try: # Find current active key old_active_key = db.query(EncryptionKey).filter( EncryptionKey.is_active == True ).first() # Generate new key raw_key = encryption_service.generate_key() encrypted_key = encryption_service.encrypt_key(raw_key) # Create new key as active new_key = EncryptionKey( id=str(uuid.uuid4()), key_data=encrypted_key, algorithm="AES-256-GCM", is_active=True, ) db.add(new_key) # Deactivate old key if exists old_key_id = None if old_active_key: old_active_key.is_active = False old_active_key.rotated_at = datetime.utcnow() old_key_id = old_active_key.id # Audit log for rotation AuditService.log_event( db=db, event_type="encryption.key_rotate", resource_type="encryption_key", action=AuditAction.UPDATE, user_id=current_user.id, resource_id=new_key.id, changes=[ {"field": "rotation", "old_value": old_key_id, "new_value": new_key.id}, {"field": "is_active", "old_value": False, "new_value": True}, ], request_metadata=getattr(request.state, "audit_metadata", None), ) db.commit() return EncryptionKeyRotateResponse( new_key_id=new_key.id, old_key_id=old_key_id, message="Key rotation completed successfully. New uploads will use the new key.", ) except MasterKeyNotConfiguredError: raise HTTPException( status_code=400, detail="Master key is not configured" ) except Exception as e: db.rollback() raise HTTPException( status_code=500, detail=f"Failed to rotate encryption key: {str(e)}" ) @router.delete("/{key_id}") async def deactivate_encryption_key( key_id: str, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_system_admin), ): """ Deactivate an encryption key. Note: This does NOT delete the key. Keys are never deleted to ensure encrypted files can always be decrypted. If this is the only active key, you must rotate to a new key first. """ key = db.query(EncryptionKey).filter(EncryptionKey.id == key_id).first() if not key: raise HTTPException(status_code=404, detail="Encryption key not found") if key.is_active: # Check if there are other active keys other_active = db.query(EncryptionKey).filter( EncryptionKey.is_active == True, EncryptionKey.id != key_id ).first() if not other_active: raise HTTPException( status_code=400, detail="Cannot deactivate the only active key. Rotate to a new key first." ) key.is_active = False key.rotated_at = datetime.utcnow() # Audit log AuditService.log_event( db=db, event_type="encryption.key_deactivate", resource_type="encryption_key", action=AuditAction.UPDATE, user_id=current_user.id, resource_id=key.id, changes=[{"field": "is_active", "old_value": True, "new_value": False}], request_metadata=getattr(request.state, "audit_metadata", None), ) db.commit() return {"detail": "Encryption key deactivated", "id": key_id}