From ed8fc0943b10d87c7d1c4e20a7bd6f7c17d32cac Mon Sep 17 00:00:00 2001 From: LGram16 Date: Tue, 3 Feb 2026 00:08:15 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B3=B4=EC=95=88=20=EA=B0=95=ED=99=94:=20DB?= =?UTF-8?q?=20=EC=9E=90=EA=B2=A9=EC=A6=9D=EB=AA=85(AppKey,=20Secret)=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=B8=EC=85=98=ED=86=A0=ED=81=B0(Access=20Token?= =?UTF-8?q?)=20=EC=95=94=ED=98=B8=ED=99=94=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(AES-GCM/CBC),=20.env=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env.example | 7 +-- .../app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 144 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 0 -> 2636 bytes backend/app/api/endpoints/settings.py | 46 +++++++++++--- .../core/__pycache__/config.cpython-312.pyc | Bin 0 -> 2691 bytes .../market_schedule.cpython-312.pyc | Bin 0 -> 2229 bytes backend/app/core/config.py | 3 + backend/app/core/crypto.py | 58 +++++++++++++++++- .../db/__pycache__/database.cpython-312.pyc | Bin 0 -> 1192 bytes .../db/__pycache__/init_db.cpython-312.pyc | Bin 0 -> 1819 bytes .../app/db/__pycache__/models.cpython-312.pyc | Bin 0 -> 12500 bytes backend/app/services/kis_auth.py | 29 ++++++--- backend/app/services/kis_client.py | 17 ++--- .../__pycache__/scheduler.cpython-312.pyc | Bin 0 -> 1857 bytes backend/run.bat | 1 + 15 files changed, 131 insertions(+), 30 deletions(-) create mode 100644 backend/app/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/__pycache__/main.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/config.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/market_schedule.cpython-312.pyc create mode 100644 backend/app/db/__pycache__/database.cpython-312.pyc create mode 100644 backend/app/db/__pycache__/init_db.cpython-312.pyc create mode 100644 backend/app/db/__pycache__/models.cpython-312.pyc create mode 100644 backend/app/workers/__pycache__/scheduler.cpython-312.pyc create mode 100644 backend/run.bat diff --git a/backend/.env.example b/backend/.env.example index c1e111a..f3b5749 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -4,11 +4,10 @@ HOST=0.0.0.0 # Security ALLOWED_HOSTS=["kis.tindevil.com", "localhost", "127.0.0.1"] -SECRET_KEY=change_this_to_a_secure_random_string +SECRET_KEY=dlrjtdmsQlalfzlfksmsep@wkf!wkf!ahfmrpTdj$# # Database DATABASE_URL=sqlite+aiosqlite:///./kis_stock.db -# KIS API (Optional here, managed in DB mostly) -# KIS_APP_KEY= -# KIS_APP_SECRET= +# Security +SECRET_KEY=change_this_to_a_secure_random_string_min_32_chars diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13466f31167acf77ea7c87341925f1d16df119c7 GIT binary patch literal 144 zcmX@j%ge<81PR+3GC}lX5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!(hs(Z3C=GqN=}Ul z_Vf*i@y;v`F3C^Mj!8;P&Q8rsiAgLdh>4HS%*!l^kJl@xyv1RYo1apelWJGQ3e?XC R#Kj=SM`lJw#v*1Q3jno-A)){P literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48ae1088842ae1f24b75d6d36808b3dea027cd1b GIT binary patch literal 2636 zcmaJ?O>7&-6`uX!?~)Wniu!YG(WWb#N+Ovql3HpSM7HFHaZC!LkN|{=#cFpXF10_z z>?*R98c^Dx4v+w~4~-0@0d(l0ffOLe_S{Q>UdUJmiA4ywC^^*}k`VxJb&L*_ga2p{r?T{FrF7GVZQtCOCA{S~N}`#TSPhLp(X?Dz7b|A!X?UgBur(AT-EoWiN< z%ig{H75m`!>HgkBNAW|pgTa?*^@>ODujJM5xsiLm`~vzllGzRT{|qvA7eOq>evS^A z&W#V)Pa-_{gsOSe@;(eSoJFi=t}WXrO#@}_Co|HBYUwq&dFO;y7d z$ni^(U4b-RzOQ+A*GmCpxKb~hHNR~_5^}? z(2t3NzT`h;_F*>cpdX=6CcfmhnJin_O)p5cv?f_NJ((_f_NDV+x+-bvE>^M}p(BVu zR*~bY5!JA>42i(bq6VhFd$BaPS~9nYj8{wf+e@prRI6m0^5*KAByVCvSp{ELbxAd* zYdcO*Q`fOolME7v!|p9~F#H4kdk;PHBO&o|_Fi$X_zj;t=2PGBp?dhGWB%nfA36yj z-_Um!J@Jd!Rpvf(g@2H~%IrpG%o;YNnmS#zOk>wKqnNU_>z^SQK-TB@Y}MAVBXp(2 zEuuL=D&wM%D@LJf52Pw8X_~o>6|n-9V68aJRF>}&MU$u-s$n^Uu8b(dVQkPiS)PQB z?TV@NwiPTXm{_}k894qvf# z4Nmcfq~qO7?@GjgR+vk7Iw;)}OdF=1UN^T4B`w+MLiUmqScldGU4+KO8R_n(^Kjj2!(7>9yM&N6ToN%{;DIrJ+15m6~ z8HSg!u5NVV8lzNYZMxghZbLXJO58TdCS-WJ*V=BMxWz=yxf)>Ni0|aY(#o=%Dkml> zirDweiO#?McJbZC1@X;dX{F>0U7x>kYq78((h(&Vm-9c(7fKEXjyc>0w%yb@0#!|G zp2q9~RTEPUS=&-@H(O3n6sgmq=mZG1Ozl1F_-iog4BH7_pD!(n3;AUyTw0l5$=`6X zAKy`-;2ob1iLprxE`?P3yCqLM{Ys~jg0JZ&9b&H&`c4ZN@W=c&0vZ@nEqYooWq^H3u&?1~1m*KR9@&9($=3O+6F*0d|i& zNg#%AqC^8FTKrfmG2TpEZX_{ICTHs?ag64k`cWi)Klt(MP2qe)IR8k< zG=-^#Fx3=t4I#G|1eYEL8`1P$_z4$nazhPn=$IP@lkep}$bTe1`0*FX-#Nu5;Wr-h^Z)sF28}E+7A^WeaRWC4;r{~3Gk`V# literal 0 HcmV?d00001 diff --git a/backend/app/api/endpoints/settings.py b/backend/app/api/endpoints/settings.py index 8d52907..20fdaf9 100644 --- a/backend/app/api/endpoints/settings.py +++ b/backend/app/api/endpoints/settings.py @@ -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 + diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d31bdb27bba8a95acd4d11ec37c852e7726f40b GIT binary patch literal 2691 zcmeHJ%WoUU8J~S{MN-s*CaH%l+ftRpVboBvBsZz7x}hY(mMu|&NTujn?R2#}l-64A zGBZoDA{anv6Tm#Tmyr_zK zNtFbSB}>lxR9{|E6%Wf+Q{J!o^8qy=01?~-hVMgQD9FDj)qJW!5uo5+eFL?5Lx?wh z%~3ioQhCNCF7++ireiZ0Oxhc_T(_`@E!vhdk!8Sj%`y$mbubf$CNzRF$aPJ7h0t@2 z)udzR%#|rqclG)%-sG=?(;0!^2?13wfGRGsyQvbAk5)HcsgtB2aub`Q0C zD6ED(bdDqTR_Ba}5jA2)$LF1UGGno!Mi@V81dXoGyZ6Kz&p7YxRzFK^Pvb)4Y&qHJ z`A|N4^_XWL`k(Ayx&!lc@mrocHZH|4Q-9;_%i#&l)mMvmOwv1{>1)U~XlQ<6?sjT2 z3)9KjR9PO>3MPx}{4{*`D$HaTzMf|npz{3OLblvAl<+=OzBQN0#w8lC99^?k9paV) zSBKy7q*u%BYbHsss2gbAv=X|Lr!C2unYo{&rXaV=#HDikD$AR(!NG6rV=r^YMuvul z%3YjInC5qUMkkV!cT(voI61eFfpZJfH>cB?^7qJlmg%Aa&2(xjW;&SQ){wZ4zLqeS zX=o~$O->{;DY&>WQw}DHsSRe3Q?#f*J3X6H=hCV2UBkkK(mg|4%b`_^+#<|9LjAA#%HKPMH(W&M8&|rr*-=TRA)5uLNBbJbxwdSV~jjE)~B2 zxQPZ$V%o&jY#mXFxR?eRz%D77?ke?>0?Q*dx!5dF+2V~a_^xIZQ3_)RKbEMZTLfQZ z+fl(J{5gAjd;c|@8C%LYMXaNx%=GO15-YR}PrIe%TD2`{g~F1~V#Ekz|b#m3GOem7~b$qsmU-R`TJ^2RDB=y;C`ij2?tWziR2+7CxE!w14~ham$rzBzphW zSFK&!;g9nN{tN$n-UOo8PI>76(3u>T|DQrUB})MJvKPVqoD4A12t7_PKx1d2VnHU* z!R89fvGS$whVt9;VeV{*ldFuR zf$URG+Cle^YL3U5yI9}mt^4|s+{@OTu29>D{5A4wHQZS}-&gItP;Kpa5%5Ko&6#Jd zK#6U^-M5c@qbH)z*RxaH&FsIjPriun{`~OD_z7UVC($SRpSqv(<*5XeYo4NKc@P`e zfBo2Zy=IDbC-BHBq*G%Oni(if$%&TAi_%L<%@SUYIFXPYncKCYZnc^WT zI)p0jMu9c*T0D3tsK2mbXtrzWwV?3lq~QjSwffrMD9rn;7b5S`ssBtc#s%IsULyL( zYguyxCz*hEYI22<6G;$+zkt9~aOo)+dJ1m5kcWky`;`}fP0v;Z5Q=_0_JQAX^l#tj Bq00aO literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/market_schedule.cpython-312.pyc b/backend/app/core/__pycache__/market_schedule.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ab6bc157f2c577a3528b027e78ea7224f241fa6 GIT binary patch literal 2229 zcmd^9&2Jk;6rY{_@YeB1iPJO`N;V)hwNj-LQ4S;p5kZ1bN%(N!V7V-t-KpZ(>vd<> zG`14Sl_E%#LyU5W4;(mBi@2umlTA4VTW)@0dxEXFgwUXmQX}C$|76H3IaRhBF;@NSBUPM^9I{gy`>JJl#LL^nml&O^MW@`n zaWg}lO0gFGICFE_a6LjxX5h^?IXE1D0?ZB~C@WYfi!C8563N0uwnU=bl`LsZI(-#1 zOp4*(PNnLJR5>HPP>rNjKQU|20`W5DBC#q(Qk#uxu-;lEX2I3+cQtR3Xf`*|@~-BT zNl9BUTw-ZXNi!>yl9H#fm^7ngg@#_WOh)-VzS8ma6-vAc1Q;ZlhsF{o!gsvL16%>f z3fB>$eI8dcvc&XVD-1S+zym+y5vJ{o*QrNUOiQ)b_^+`rC&~sj?BKU2wBffMo48)y z9M&AF4Zr_6p)N7p;S+mb4J~{%)XtjXgRAL6a^ccIG&j-lPE z;GJkK(UQV$T*YVP@@bc@iYs_UF!3@jTtR4Apt68gM8;>3i3OnP$lM{Tp_xN=F=f#l zCK-7NNLr>WXkRQj_kHnpX5Nq7C!}B*RmwtqDA9hVjs=~e^PZPo&!C70H2|W4 z7j~AvTHda$#||#`H9l+#+tGFTAko`+wLv$F>*CX7Uo-YYa^zd_Npj?|I?|!^B0TWF zL74VJv@Gz3;_aRg%6prIe}!@`9raaRFBvw`bw8@>w!`K-@G)J#S22o# zCZfa4GMN~v6h@y-yhX?IMc1RgwBR^J%8H*?DrHeVM7&Zu^~>fSAAWqM>2O*@M;Oa$ zTSZE58>{lArrSoaedGPcwqr4rxBEC3lW#OZ1zVs3uKglcB|0kDxatT4*Lj;gn^t)z zazuQoT=ie6{H-WT8K@fgM zul$61o+_!j^`-s5ZoJo0hU(&e{L1?L)}4(z&0DSb^?LLVX;ipzgc$iuK&hct@5>L8 H{Dr>&y-n&% literal 0 HcmV?d00001 diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 2f6e3b0..033c8b3 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -29,6 +29,9 @@ class Settings(BaseSettings): # Timezone TIMEZONE: str = "Asia/Seoul" + # Encryption + SECRET_KEY: str = "dlrpwjdakfehlsmswl_skf!wkf!ahfmrpTDJ!@#unsafe_default_key_change_in_production_min_32_bytes" + model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", diff --git a/backend/app/core/crypto.py b/backend/app/core/crypto.py index b2cbb82..1eaa1d0 100644 --- a/backend/app/core/crypto.py +++ b/backend/app/core/crypto.py @@ -1,7 +1,10 @@ from Crypto.Cipher import AES -from Crypto.Util.Padding import unpad -from base64 import b64decode +from Crypto.Util.Padding import unpad, pad +from base64 import b64decode, b64encode +from app.core.config import settings +import hashlib +# KIS WebSocket Decryption (CBC) def aes_cbc_base64_dec(key: str, iv: str, cipher_text: str) -> str: """ Decrypts KIS WebSocket data using AES-256-CBC. @@ -17,3 +20,54 @@ def aes_cbc_base64_dec(key: str, iv: str, cipher_text: str) -> str: decrypted_bytes = unpad(cipher.decrypt(b64decode(cipher_text)), AES.block_size) return bytes.decode(decrypted_bytes, 'utf-8') + +# DB Field Encryption (AES-GCM) +def get_master_key(): + # Derive a 32-byte key from the SECRET_KEY string, ensuring length + return hashlib.sha256(settings.SECRET_KEY.encode('utf-8')).digest() + +def encrypt_str(plain_text: str) -> str: + """ + Encrypts string for DB storage using AES-GCM. + Returns: base64(nonce + ciphertext + tag) + """ + if not plain_text: + return "" + + key = get_master_key() + cipher = AES.new(key, AES.MODE_GCM) + nonce = cipher.nonce # 16 bytes + + ciphertext, tag = cipher.encrypt_and_digest(plain_text.encode('utf-8')) + + # Combined: nonce(16) + tag(16) + ciphertext(n) + combined = nonce + tag + ciphertext + return b64encode(combined).decode('utf-8') + +def decrypt_str(encrypted_text: str) -> str: + """ + Decrypts string from DB. + Input: base64(nonce + tag + ciphertext) + """ + if not encrypted_text: + return "" + + try: + raw = b64decode(encrypted_text) + if len(raw) < 32: # Nonce(16) + Tag(16) + return "" # Invalid data + + nonce = raw[:16] + tag = raw[16:32] + ciphertext = raw[32:] + + key = get_master_key() + cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) + + decrypted_data = cipher.decrypt_and_verify(ciphertext, tag) + return decrypted_data.decode('utf-8') + except Exception: + # Failed to decrypt (possibly not encrypted or wrong key). + # For safety, return empty or raise. + # In transition phase, might check if plain text? No, assume encrypted. + return "[Decryption Failed]" diff --git a/backend/app/db/__pycache__/database.cpython-312.pyc b/backend/app/db/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..142bca1110c98457cceca4f67525d26e33698df6 GIT binary patch literal 1192 zcmZuw-D@0G6hC)9cQ>;iX-Ps-6hDy_U5p5|;s;x*BCSEP3hA&g%+B0y#+{G!-q|LL zx&#}fVlgP%JowTlMbZ}Wq59-M;7jw=;bQT@7k#sAz#u+&?(9Y_c$oRk@0@$)+;i?Z z_s4R%1Sp@~+zdbk_(dv*Vob>BTO>Q+gHJ+;h)p8JRwC6_iL@!97U{O0b2T&~(>4hx zKnXS9t{sN@_v>rE{{Jf%fN8$rYXON#0N2c2%twk{#ApgFQWgKzt`&B*iM^!2tKb*6 zz%Nn#AMYuDY71;@VAadNNyoY>j(Uu`DRo>vh&_kK-5{po$XMf)^B{>Mw?~<XN!J%_syb<$Pb%C9Rzquy#F zEH8;;>ZOj$y1cFltrNt)Ffb_Rj;PU%wSZA4i5)MAq97GTx1T2G!aiU1#zl^iOq7v@ zWCtkN#0Og$5V?^|YzeC@I8H26a74**qQviql3R{*zVC+Pm9JqWBmMbw>$!F-=`)YE zTd%$TM*DQYTWR9;+8x*H(b#XhYin)4)Arre?O^!E+JH^tk2%aD;X1+=WLkkqHMifB zv58hy0T_LZIz6lccF`u~8Mugbyn}tdqU@+2=sLUy@4|HrHImO0&etvmk#bSF z*OR~ij>F{ze9ebpalDR`aYdDpj)M-ItSbc0;@;ACLp=-YSPJ~Xid6&Ch^=5 zb#pQZTxR(In?n>vm!^*2VaHHez?VOU@GBf_!cWy>+r!Jlt8-ap=A-f-D%9@Z9)37H zxHLT29!mBXe-?e;uQqk^33A`>COKh!XCnE* zMAK5eEM{@K+z>}A8Z_ue+Q`Q>NSM48OkM#ddmt+RaZ6al|eu67)6NB1zjfF zA*@r)H=8G$t(Tp*md^-dd=J>X43#%mmdR!$7l3rvF;(|OI-b46ZC4~G8 zD|@hV2OhrzkK8Rl>Ap;}Zfo;<+I&{5WQ!-hU%CCl@*X~m%eRg^dbPhd{p76`d*9Sb s%BFF*0=0*-Vl|tZ%Vrj`%F+FzUQ;%${Td;~OE2vMentg_%;w4b4YlwqQ2+n{ literal 0 HcmV?d00001 diff --git a/backend/app/db/__pycache__/init_db.cpython-312.pyc b/backend/app/db/__pycache__/init_db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e4d0442b15e18dc38b382380dac39ee9c669b0b GIT binary patch literal 1819 zcmah~Uq~EB7@xWQck1eSmx#IM{DYd*D{5k@F->BO;d}_zQ1B_o!Lja6lRbC$?Cy%W z^t^=H76suND3Q`Y^4MTXUrOmy!Q`aSyeL{umV~A>h34(RLTuh}xR}-adQH3rF!wQv*Jk*r@Qf}P#SU)~jmUPCad=`}QVrs;=Ol|b;v}&w zHEq0`mL(11>PBbONn+coamk|7^t3`WORmszjR5Te)ZK4^?Kr}~)5cyv$~%GQ0J~bj z184y&=tDU3P2URhDaWCCltn4-rO*nrdC!;fd#Gwt?)_E!6s=YmEWkavJ22%P+$}_)a=5Um~@Qlg@^zBoXtB1}Ts;x?zdX*^c3VC0e)$I0{ z(x6*ahWn$@MiXcuhnpghK91jOWKAa`Wa zoN>6h%fuvR9mN8`jUPtc6;KBiDG16GcvH%Fj({tZukeEbEvU3pZl{GHAdX%=BI(C&!@Kn z9mPQRW}y4t!EajcwtnBg89luf7+z#|np;<{-MDtg-fHeC274Cyop4(*d~7p(Y%4sl z*zjwhoXCj<_#BzBImZpnF@4?u5cCeK-2HLLEi1V?*qCBnCG^?`(@F zpEtV{7IURK0DnF45AKyHtG#?KpIgekC;_+Dy%LW?y?dnwRNuK*YN`(ZZT$d;{wO?l z0Dp;NZTu|;(CyyXVP;Llu?~K%o~1Zg2kx~9j&-qXEg_1J;MggC?I;ajdwY}}W0?Cx zI5y1RKQYQf=fN2q8{r>}1OOKr=kL2EB&^rqF;QHv6(|l0;J*;9b84**G)a>$6FuX~(+R+X_rXI` z1y8|m!IiqJY*)ihePz7-Hz=!_FZ)dTfp%4K7wrN>W&|3?e;Q(EHFC!6f-cQAi+-<4 d4955|YWW2n-$chBqeH)=@Gcv~r_4?m{Rd27pH%<= literal 0 HcmV?d00001 diff --git a/backend/app/db/__pycache__/models.cpython-312.pyc b/backend/app/db/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f39e849cd01957bb26532d47713f9f7fc78abf2 GIT binary patch literal 12500 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 gk<`Uw3mlGHtE|_pD2B)5{uZkboUd7XSbN literal 0 HcmV?d00001 diff --git a/backend/app/services/kis_auth.py b/backend/app/services/kis_auth.py index 62396f5..fa17e67 100644 --- a/backend/app/services/kis_auth.py +++ b/backend/app/services/kis_auth.py @@ -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 diff --git a/backend/app/services/kis_client.py b/backend/app/services/kis_client.py index a0253a7..78e4f3b 100644 --- a/backend/app/services/kis_client.py +++ b/backend/app/services/kis_client.py @@ -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 diff --git a/backend/app/workers/__pycache__/scheduler.cpython-312.pyc b/backend/app/workers/__pycache__/scheduler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f0e95be0c4713ccf7e5bb60c70273d28bd30eff GIT binary patch literal 1857 zcmb7F&2Jk;6rZuz_Uk`SXv+e8Avf!spOfpS2+H(sxa0Rdypo44=1`FOwg z;jgn;Db^xp^OV`34DHi<(klp>#KCY)p?Dfnbla#EF);H75T z$y73ePc`L=oFGiDpB)}LTA8{<2Va#5u%TO#sxoP%-y@Z*l>wTwWS~=)0(82QDym0l zRixo`$?v%4&DFBG&a8Hmaga?p25+#S=0`FRsSVq&nN6Fy0dC^VXa=^!YL4L=YmBd$ z(WPQRhu?+SzYk=IP;feko=8YNA#6?&iw<3QhrCVr)j%4m>j38X@Mi;gD63BlxzTDR zhNn@|N#gJGi^Zi=5Jy2Tp0WubH0fGBaS7 zwz1A!Z2$}^ycTnHab76D;yKI@Y;*J4vH%YCtDwK2l>>tZw!8M1v-Oo?nxmIt4uFmD z#xprO9Ad>~4efmdR{L$CyjU%JZEmt^`R4Mi>XPl3 z1J7(!ZyRQVxmML^wW=E)gq8c%VO+W1>V(tL zK00)_L`n&IDoHR6M3PJbGf0vS!9YEvD-Q|BMDN2SJO?DqxopFSgycp`ym1DEPr&Ts zL&Y19(M5{Am@P&MjfSNW0d0-wGy8tw;~(*Z~NkkfPK>c*caCf>`VQ_zQ3uat?1qG zE{v2Ibxe`5VWt06b)=YC31!<27=O!X9w9K0ycx%K+X3^BuvZ>o;%=Ue`_7C}%%ovsxXC=q5bBscm^0BlyKA7XX#^X07^l=O0{2~}#hUr&;bjg8~-IwyaQoc8P z<$<~lDV^_0H)82W?x*$bi$eHfEbPq|A1rRaDWs(zrR=@cJF7h@e;}#%mhLR=Okes| zy38?!if6*}Mk{tgk2zmAu-)1o$7qLhP?fsrai&K_7#oV#P(Za05ZWg5^-%}rh-diJ zDeZVij`|3;59}k#8Vg<>v=X^ia}8+SwE=if!&#!_N8k#q#sQUIa6J5jxWTW%4h)|E zGLYjWrSyEv)d_;2JDf9OW`^x;TGXKfKo^q+1{(kbwy~*=k