보안 강화: DB 자격증명(AppKey, Secret) 및 세션토큰(Access Token) 암호화 저장 구현 (AES-GCM/CBC), .env 정리

This commit is contained in:
2026-02-03 00:08:15 +09:00
parent 4f0cc05f39
commit ed8fc0943b
15 changed files with 131 additions and 30 deletions

View File

@@ -19,6 +19,8 @@ class SettingsSchema(BaseModel):
class Config:
from_attributes = True
from app.core.crypto import encrypt_str, decrypt_str
@router.get("/", response_model=SettingsSchema)
async def get_settings(db: AsyncSession = Depends(get_db)):
stmt = select(ApiSettings).where(ApiSettings.id == 1)
@@ -26,7 +28,25 @@ async def get_settings(db: AsyncSession = Depends(get_db)):
settings = result.scalar_one_or_none()
if not settings:
raise HTTPException(status_code=404, detail="Settings not initialized")
return settings
# Clone logic to mask secrets for display
# We can't easily clone SQLA model to Pydantic and modify without validation error if strict.
# So we construct dict.
resp_data = SettingsSchema.model_validate(settings)
if settings.appKey: resp_data.appKey = "********" # Masked
if settings.appSecret: resp_data.appSecret = "********" # Masked
if settings.accountNumber:
# Decrypt first if we want to show last 4 digits?
# Or just show encrypted? Users prefer masking.
# Let's decrypt and mask everything except last 2.
pass # Keep simple for now: Mask all or partial?
# User just sees *****
# Actually proper UX is to leave field blank or show placeholder.
# Let's mask entirely for keys.
return resp_data
@router.put("/", response_model=SettingsSchema)
async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(get_db)):
@@ -38,16 +58,26 @@ async def update_settings(payload: SettingsSchema, db: AsyncSession = Depends(ge
settings = ApiSettings(id=1)
db.add(settings)
# Update fields if provided
if payload.appKey is not None: settings.appKey = payload.appKey
if payload.appSecret is not None: settings.appSecret = payload.appSecret
if payload.accountNumber is not None: settings.accountNumber = payload.accountNumber
# Update fields if provided - ENCRYPT SENSITIVE DATA
if payload.appKey is not None:
settings.appKey = encrypt_str(payload.appKey)
if payload.appSecret is not None:
settings.appSecret = encrypt_str(payload.appSecret)
if payload.accountNumber is not None:
settings.accountNumber = encrypt_str(payload.accountNumber)
if payload.kisApiDelayMs is not None: settings.kisApiDelayMs = payload.kisApiDelayMs
await db.commit()
await db.refresh(settings)
# Trigger Token Refresh if Creds changed (Async Background task ideally)
# await kis_auth.get_access_token(db)
# Return masked object check
# We return what was saved, but masked?
# Usually convention is to return updated state.
resp = SettingsSchema.model_validate(settings)
resp.appKey = "********"
resp.appSecret = "********"
resp.accountNumber = "********"
return settings
return resp