보안 강화: 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

@@ -3,6 +3,8 @@ from datetime import datetime, timedelta
from sqlalchemy import select
from app.db.database import SessionLocal
from app.db.models import ApiSettings
from app.db.models import ApiSettings
from app.core.crypto import decrypt_str, encrypt_str
class KisAuth:
BASE_URL_REAL = "https://openapi.koreainvestment.com:9443"
@@ -29,21 +31,26 @@ class KisAuth:
if not settings_obj or not settings_obj.appKey or not settings_obj.appSecret:
raise ValueError("KIS API Credentials not configured.")
# 2. Check Expiry (Buffer 10 mins)
# 2. Check Expiry (Buffer 10 mins)
if settings_obj.accessToken and settings_obj.tokenExpiry:
if settings_obj.tokenExpiry > datetime.now() + timedelta(minutes=10):
return settings_obj.accessToken
token_dec = decrypt_str(settings_obj.accessToken)
if token_dec and token_dec != "[Decryption Failed]":
if settings_obj.tokenExpiry > datetime.now() + timedelta(minutes=10):
return token_dec
# 3. Issue New Token
token_data = await self._issue_token(settings_obj.appKey, settings_obj.appSecret)
app_key_dec = decrypt_str(settings_obj.appKey)
app_secret_dec = decrypt_str(settings_obj.appSecret)
token_data = await self._issue_token(app_key_dec, app_secret_dec)
# 4. Save to DB
settings_obj.accessToken = token_data['access_token']
# 4. Save to DB (Encrypt Token)
settings_obj.accessToken = encrypt_str(token_data['access_token'])
# expires_in is seconds (usually 86400)
settings_obj.tokenExpiry = datetime.now() + timedelta(seconds=int(token_data['expires_in']))
await db_session.commit()
return settings_obj.accessToken
return token_data['access_token']
except Exception as e:
await db_session.rollback()
@@ -83,12 +90,16 @@ class KisAuth:
raise ValueError("KIS API Credentials not configured.")
if settings_obj.websocketApprovalKey:
return settings_obj.websocketApprovalKey
approval_key_dec = decrypt_str(settings_obj.websocketApprovalKey)
if approval_key_dec and approval_key_dec != "[Decryption Failed]":
return approval_key_dec
# Issue New Key
approval_key = await self._issue_approval_key(settings_obj.appKey, settings_obj.appSecret)
app_key_dec = decrypt_str(settings_obj.appKey)
app_secret_dec = decrypt_str(settings_obj.appSecret)
approval_key = await self._issue_approval_key(app_key_dec, app_secret_dec)
settings_obj.websocketApprovalKey = approval_key
settings_obj.websocketApprovalKey = encrypt_str(approval_key)
await db_session.commit()
return approval_key

View File

@@ -5,6 +5,7 @@ from app.core.rate_limiter import global_rate_limiter
from app.db.database import SessionLocal
from app.db.models import ApiSettings
from sqlalchemy import select
from app.core.crypto import decrypt_str
class KisClient:
"""
@@ -51,8 +52,8 @@ class KisClient:
headers = {
"Content-Type": "application/json",
"authorization": f"Bearer {token}",
"appkey": settings.appKey,
"appsecret": settings.appSecret,
"appkey": decrypt_str(settings.appKey),
"appsecret": decrypt_str(settings.appSecret),
"tr_id": tr_id,
"tr_cont": "",
"custtype": "P"
@@ -106,7 +107,7 @@ class KisClient:
# -----------------------------
async def get_balance(self, market: str) -> Dict:
settings = await self._get_settings()
acc_no = settings.accountNumber
acc_no = decrypt_str(settings.accountNumber)
# acc_no is 8 digits. Split? "500xxx-01" -> 500xxx, 01
if '-' in acc_no:
cano, prdt = acc_no.split('-')
@@ -156,11 +157,13 @@ class KisClient:
price: 0 for Market? KIS logic varies.
"""
settings = await self._get_settings()
if '-' in settings.accountNumber:
cano, prdt = settings.accountNumber.split('-')
acc_no_str = decrypt_str(settings.accountNumber)
if '-' in acc_no_str:
cano, prdt = acc_no_str.split('-')
else:
cano = settings.accountNumber[:8]
prdt = settings.accountNumber[8:]
cano = acc_no_str[:8]
prdt = acc_no_str[8:]
if market == "Domestic":
# TR_ID: TTT 0802U (Buy), 0801U (Sell) -> using sample 0012U/0011U