feat(#28): Gate B [08/10]: AES-GCM settings round-trip (AES-1, AES-2)

- AES-GCM encrypt/decrypt module (app/security/aes.py):
  - get_aes_key: decode base64 AES master key to 32 bytes
  - encrypt_setting: AES-256-GCM with fresh 12-byte nonce per call, returns nonce||ciphertext
  - decrypt_setting: decrypts ciphertext, raises AuthenticationError on tamper/wrong key
- Settings endpoint (app/api/settings.py):
  - GET /api/v1/settings: returns SettingsView with decrypted values, secrets masked
    (woo_app_password never returned, only woo_credentials_configured boolean)
  - PUT /api/v1/settings: encrypts all setting values AES-GCM, stores in settings table
  - Rejects woo_consumer_key/woo_consumer_secret fields
  - Validates woo_create_status (draft|publish)
- App factory (app/main.py): stores settings on app.state for dependency injection
- Unit tests (tests/unit/test_aes.py): 18 tests covering AES-1 (round-trip), AES-2
  (missing key refusal), FUND-17 (fresh nonce, tamper rejection), FUND-19
  (no plaintext fallback)

Test results: 23/23 passed (18 new AES + 5 existing health tests)
This commit is contained in:
grokbot_ty
2026-09-07 08:00:29 +00:00
parent e5514fe3bf
commit 82219fe5ad
5 changed files with 526 additions and 7 deletions
+12 -7
View File
@@ -5,7 +5,8 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.api import health
from app.api import health as health_router
from app.api import settings as settings_router
from app.config import Settings
from app.db.engine import close_db_connection, init_db
from app.logging_config import configure_logging
@@ -26,24 +27,28 @@ async def lifespan(app: FastAPI):
await close_db_connection()
def create_app(settings: Settings | None = None) -> FastAPI:
def create_app(app_settings: Settings | None = None) -> FastAPI:
"""Create a fresh FastAPI app instance.
Each call produces an independent app with its own database connection
and cleared dependency overrides. This is the factory used by tests.
Args:
settings: Application settings. Uses env vars if None.
app_settings: Application settings. Uses env vars if None.
Returns:
Configured FastAPI application.
"""
if settings is None:
settings = Settings()
settings.validate()
if app_settings is None:
app_settings = Settings()
app_settings.validate()
app = FastAPI(title="SlabUploader", version="0.1.0", lifespan=lifespan)
app.include_router(health.router)
# Store settings on app state for dependency injection
app.state.settings = app_settings
app.include_router(health_router.router)
app.include_router(settings_router.router)
return app