Wave J: 카드보안·자금·규제 고도화 (패리티 70%→80%)

패리티 분석의 최우선 갭(카드보안·자금연동·운영·규제)을 데이터모델+시드+
런타임서비스+화면으로 체계적 보강. 전부 실측·런타임 검증.

■ J-1 카드보안 (승인 60→82)
- card_vault(6,680): PCI 토큰볼트 — PAN 원문 미저장, 마스킹+SHA256 해시.
- au_3ds(1,235): 3DS/SCA — ECI·CAVV·책임전환(ISSUER 1,143).
- au_verify(8,191): AVS/CVV + 리스크판정(APPROVE/REVIEW/DECLINE).
- 서비스 TOKENIZE/AUTH_3DS/AUTH_VERIFY (au).

■ J-2 자금이동 (정산 75→85)
- st_funding_instr(355k): 펌뱅킹 펀딩지시/반환(CONFIRMED/RETURNED).
- ac_dcc(426): DCC 다통화 승인시 환율고정(USD/JPY/EUR/CNY + 마크업 3%).
- 서비스 ST_FUNDINSTR/AC_DCC.

■ J-3 수수료·분쟁 (수수료 80→88, 분쟁 70→82)
- st_cb_fee(175): 채그백 건당수수료(단계별 차등).
- ac_retrieval(127) + 분쟁 SLA타이머·fraud_flag. 서비스 ACQ_RETRIEVAL.

■ J-4 리포팅·규제 (리포팅 70→85)
- rp_merchant_stmt(12.8k): 가맹점 월명세서.
- rp_regulatory(134): 여신전문금융업법 정산주기 준수·거래량 보고.

■ J-5 화면·검증
- 포털 "카드보안·규제"(SY0004, #sec): 토큰볼트·3DS·AVS/CVV·DCC·펀딩·규제.
- 통합빌드 error 0, 5,907 서비스 AVAIL, 6본 신규서비스 실호출 검증(미결2PC 0).
- PARITY_ANALYSIS 재채점: 가중 ≈80/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyeongwoo 2026-07-23 13:00:07 +09:00
parent 782b802156
commit d292772b1f
21 changed files with 1337 additions and 7 deletions

View file

@ -94,6 +94,10 @@
#include "RISK_SCORE_INQ.h"
#include "APPROVAL_RATE.h"
#include "AUTH_DUP_CHK.h"
/* --- Wave J: 카드보안 승인 3본 (au) --- */
#include "TOKENIZE.h"
#include "AUTH_3DS.h"
#include "AUTH_VERIFY.h"
/* --- scale-out services (승인/한도 심화) --- */
#include "LIMIT_MONTH_CHK.h"
#include "LIMIT_MONTH_INQ.h"
@ -194,6 +198,9 @@ static struct { const char *name; void (*fn)(TPSVCINFO *); } SVCS[] = {
{"MERCH_AUTH_RANK", MERCH_AUTH_RANK}, {"CARD_DAILY_RPT", CARD_DAILY_RPT},
{"RISK_SCORE_INQ", RISK_SCORE_INQ}, {"APPROVAL_RATE", APPROVAL_RATE},
{"AUTH_DUP_CHK", AUTH_DUP_CHK},
/* --- Wave J: 카드보안 승인 3본 (au) --- */
{"TOKENIZE", TOKENIZE}, {"AUTH_3DS", AUTH_3DS},
{"AUTH_VERIFY", AUTH_VERIFY},
/* --- scale-out services (승인/한도 심화) --- */
{"LIMIT_MONTH_CHK", LIMIT_MONTH_CHK}, {"LIMIT_MONTH_INQ", LIMIT_MONTH_INQ},
{"LIMIT_PERTX_CHK", LIMIT_PERTX_CHK}, {"LIMIT_CUMUL_INQ", LIMIT_CUMUL_INQ},

24
app/src/au/svc/AUTH_3DS.h Normal file
View file

@ -0,0 +1,24 @@
/*
* AUTH_3DS.h - au AUTH_3DS (copybook / record header).
* : 3DS/SCA · (ECI auth_status/liability, ).
* Request fields: T_PURCHASE_ID( ID), T_STR1(eci: 05/06/07)
* Response fields: T_STATUS(auth_status: Y/A/N/U), T_STR2(liability: ISSUER/MERCHANT),
* T_ID1(tds_id), T_RC, T_MSG
*/
#ifndef AUTH_3DS_H
#define AUTH_3DS_H
#include "au_svc.h"
typedef struct {
long tds_id;
long purchase_id;
char version[16];
char eci[8];
char auth_status[8]; /* Y=인증 A=시도 N=실패 U=불가 */
char liability[16]; /* ISSUER(전환됨) / MERCHANT */
} au_3ds_rec_t;
void AUTH_3DS(TPSVCINFO *p);
#endif /* AUTH_3DS_H */

124
app/src/au/svc/AUTH_3DS.pgc Normal file
View file

@ -0,0 +1,124 @@
/*
* AUTH_3DS.pgc - au 모듈 서비스 AUTH_3DS (one service = one file).
* 3DS/SCA 비대면 인증결과 기록·판정: 입력 ECI 를 인증상태(auth_status)와
* 책임주체(liability)로 매핑하여 au_3ds 원장에 적재한다.
* ECI 05 → Y(완전인증) / ISSUER(책임전환)
* ECI 06 → A(시도) / ISSUER(책임전환)
* ECI 07 → N(미인증) / MERCHANT(가맹점 책임)
* 그 외 → U(불가) / MERCHANT
* 처리단계: 입력검증 -> 매입/기존인증 조회 -> ECI 판정분기 -> 본처리(INSERT) ->
* 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* XA 격리: 체인 내 미커밋 매입은 이 브랜치에서 안 보일 수 있으므로 조회 미존재(100)는
* 정상 흐름으로 간주하고 FAIL 하지 않는다(owner-writes 규율, RECONCILE 패턴).
*/
#include "AUTH_3DS.h"
/* AUTH_3DS - 3DS/SCA 인증결과 기록·판정. */
void AUTH_3DS(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "AUTH_3DS";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0;
char inbuf[16];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_eci[8];
char h_astat[8];
char h_liab[16];
char h_ver[16];
long h_tds;
long h_prior;
long h_vcnt;
long h_pcnt;
EXEC SQL END DECLARE SECTION;
memset(inbuf, 0, sizeof(inbuf));
h_pid = 0; h_tds = 0; h_prior = 0; h_vcnt = 0; h_pcnt = 0;
memset(h_eci, 0, sizeof(h_eci)); memset(h_astat, 0, sizeof(h_astat));
memset(h_liab, 0, sizeof(h_liab)); memset(h_ver, 0, sizeof(h_ver));
strcpy(h_ver, "2.2.0");
userlog("[%s] 진입: 3DS/SCA 인증결과 기록·판정", svcname);
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_STR1, inbuf, sizeof(inbuf));
if (h_pid <= 0) {
setl(b, T_RC, 8401L); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[%s] 입력검증 실패: pid=%ld", svcname, h_pid); FAIL(b);
}
if (inbuf[0] == 0) {
setl(b, T_RC, 8402L); Bchg(b, T_MSG, 0, "ECI 필수 누락", 0L);
userlog("[%s] 입력검증 실패: ECI 누락 pid=%ld", svcname, h_pid); FAIL(b);
}
strncpy(h_eci, inbuf, sizeof(h_eci) - 1); h_eci[sizeof(h_eci) - 1] = 0;
/* ===== 2) 조회부 (매입 존재 + 기존 인증/검증 현황) =====
* 매입 미존재(100)는 체인 신규건일 수 있어 정상 처리. */
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8410L); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L);
userlog("[%s] 매입 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
if (h_pcnt == 0)
userlog("[%s] 매입 미커밋/미존재 pid=%ld — 인증만 기록", svcname, h_pid);
EXEC SQL SELECT count(*) INTO :h_prior FROM au_3ds WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_prior = 0; }
EXEC SQL SELECT count(*) INTO :h_vcnt FROM au_verify WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_vcnt = 0; }
if (h_prior > 0)
userlog("[%s] 기존 3DS 인증 %ld건 존재 pid=%ld — 재인증 기록", svcname, h_prior, h_pid);
/* ===== 3) ECI 판정 분기부 (auth_status / liability) ===== */
if (strcmp(h_eci, "05") == 0) {
strcpy(h_astat, "Y"); strcpy(h_liab, "ISSUER"); rc = 0;
} else if (strcmp(h_eci, "06") == 0) {
strcpy(h_astat, "A"); strcpy(h_liab, "ISSUER"); rc = 0;
} else if (strcmp(h_eci, "07") == 0) {
strcpy(h_astat, "N"); strcpy(h_liab, "MERCHANT"); rc = 2;
userlog("[%s] 미인증(ECI07) — 책임 가맹점 pid=%ld", svcname, h_pid);
} else {
strcpy(h_astat, "U"); strcpy(h_liab, "MERCHANT"); rc = 1;
userlog("[%s] 미지원 ECI=[%s] — 불가(U)/가맹점 pid=%ld", svcname, h_eci, h_pid);
}
/* ===== 4) 본처리부 (au_3ds 채번·INSERT) ===== */
EXEC SQL SELECT nextval('au_3ds_seq') INTO :h_tds;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8420L); Bchg(b, T_MSG, 0, "3DS 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO au_3ds
(tds_id, purchase_id, version, eci, auth_status, liability)
VALUES (:h_tds, :h_pid, :h_ver, :h_eci, :h_astat, :h_liab);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8421L); Bchg(b, T_MSG, 0, "3DS 인증결과 적재 실패", 0L);
userlog("[%s] au_3ds 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 5) 적재 재검증부 (기록행 재조회) ===== */
EXEC SQL SELECT auth_status, liability INTO :h_astat, :h_liab
FROM au_3ds WHERE tds_id = :h_tds;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8430L); Bchg(b, T_MSG, 0, "3DS 적재 재검증 실패", 0L);
userlog("[%s] 재검증 실패 tds=%ld code=%d", svcname, h_tds, sqlca.sqlcode); FAIL(b);
}
/* ===== 6) 응답부 ===== */
Bchg(b, T_STATUS, 0, h_astat, 0L); /* auth_status: Y/A/N/U */
Bchg(b, T_STR2, 0, h_liab, 0L); /* liability: ISSUER/MERCHANT */
setl(b, T_ID1, h_tds);
setl(b, T_RC, rc);
Bchg(b, T_MSG, 0,
(strcmp(h_liab, "ISSUER") == 0 ? "3DS 인증 완료(책임전환)" : "3DS 미전환(가맹점 책임)"),
0L);
/* ===== 7) 완료 로깅 ===== */
userlog("[%s] 완료: pid=%ld eci=%s status=%s liability=%s tds=%ld rc=%ld",
svcname, h_pid, h_eci, h_astat, h_liab, h_tds, rc);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

View file

@ -0,0 +1,23 @@
/*
* AUTH_VERIFY.h - au AUTH_VERIFY (copybook / record header).
* : AVS/CVV + (risk_score APPROVE/REVIEW/DECLINE).
* Request fields: T_PURCHASE_ID( ID), T_STR1(avs_code), T_STR2(cvv_code)
* Response fields: T_STATUS(decision), T_ARG1(risk_score), T_ID1(vrf_id), T_RC, T_MSG
*/
#ifndef AUTH_VERIFY_H
#define AUTH_VERIFY_H
#include "au_svc.h"
typedef struct {
long vrf_id;
long purchase_id;
char avs_code[8]; /* Y/A/Z/N/U */
char cvv_code[8]; /* M/N/P/U */
long risk_score; /* 0~999 */
char decision[16]; /* APPROVE / REVIEW / DECLINE */
} au_verify_rec_t;
void AUTH_VERIFY(TPSVCINFO *p);
#endif /* AUTH_VERIFY_H */

View file

@ -0,0 +1,138 @@
/*
* AUTH_VERIFY.pgc - au 모듈 서비스 AUTH_VERIFY (one service = one file).
* AVS/CVV 검증 + 리스크 판정: 주소검증(AVS)·카드검증값(CVV) 코드로 리스크 점수를
* 산출하고, 임계값으로 승인판정(decision)을 내려 au_verify 원장에 적재한다.
* risk_score > 900 → DECLINE, > 750 → REVIEW, 그 외 → APPROVE.
* AVS/CVV 응답 품질이 나쁠수록 가점(위험↑). 3DS 책임미전환(MERCHANT)이면 가중.
* 처리단계: 입력검증 -> 매입/기존검증/3DS 조회 -> 리스크 산출 -> 판정분기 ->
* 본처리(INSERT) -> 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* XA 격리: 체인 내 미커밋 매입은 안 보일 수 있어 미존재(100)는 정상 처리한다.
*/
#include "AUTH_VERIFY.h"
/* AUTH_VERIFY - AVS/CVV 검증 + 리스크 판정. */
void AUTH_VERIFY(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "AUTH_VERIFY";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0, risk = 0;
char avsbuf[8], cvvbuf[8];
EXEC SQL BEGIN DECLARE SECTION;
long h_pid;
char h_avs[8];
char h_cvv[8];
long h_score;
char h_dec[16];
long h_vrf;
long h_prior;
long h_pcnt;
char h_liab[16];
EXEC SQL END DECLARE SECTION;
memset(avsbuf, 0, sizeof(avsbuf)); memset(cvvbuf, 0, sizeof(cvvbuf));
h_pid = 0; h_score = 0; h_vrf = 0; h_prior = 0; h_pcnt = 0;
memset(h_avs, 0, sizeof(h_avs)); memset(h_cvv, 0, sizeof(h_cvv));
memset(h_dec, 0, sizeof(h_dec)); memset(h_liab, 0, sizeof(h_liab));
strcpy(h_liab, "NONE");
userlog("[%s] 진입: AVS/CVV 검증 + 리스크 판정", svcname);
/* ===== 1) 입력 검증부 ===== */
h_pid = getl(b, T_PURCHASE_ID);
gets_(b, T_STR1, avsbuf, sizeof(avsbuf));
gets_(b, T_STR2, cvvbuf, sizeof(cvvbuf));
if (h_pid <= 0) {
setl(b, T_RC, 8501L); Bchg(b, T_MSG, 0, "매입 ID(purchase_id) 필수", 0L);
userlog("[%s] 입력검증 실패: pid=%ld", svcname, h_pid); FAIL(b);
}
if (avsbuf[0] == 0) strcpy(avsbuf, "U"); /* 미제공 → 불가(U) */
if (cvvbuf[0] == 0) strcpy(cvvbuf, "U");
strncpy(h_avs, avsbuf, sizeof(h_avs) - 1); h_avs[sizeof(h_avs) - 1] = 0;
strncpy(h_cvv, cvvbuf, sizeof(h_cvv) - 1); h_cvv[sizeof(h_cvv) - 1] = 0;
/* ===== 2) 조회부 (매입 존재 + 기존 검증/3DS 책임 현황) ===== */
EXEC SQL SELECT count(*) INTO :h_pcnt FROM purchase WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8510L); Bchg(b, T_MSG, 0, "매입 조회 오류", 0L);
userlog("[%s] 매입 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
if (h_pcnt == 0)
userlog("[%s] 매입 미커밋/미존재 pid=%ld — 검증만 기록", svcname, h_pid);
EXEC SQL SELECT count(*) INTO :h_prior FROM au_verify WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0) { h_prior = 0; }
EXEC SQL SELECT coalesce(max(liability), 'NONE') INTO :h_liab
FROM au_3ds WHERE purchase_id = :h_pid;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) { strcpy(h_liab, "NONE"); }
/* ===== 3) 리스크 산출부 (AVS/CVV 품질 → 가점) =====
* AVS: Y 일치=0, A 부분=50, Z 우편만=80, N 불일치=200, U 불가=150.
* CVV: M 일치=0, N 불일치=400, P 미처리=150, U 불가=150.
* 3DS 책임 미전환(MERCHANT)면 +100. pid 기반 소폭 분산 후 0~999 로 클램프. */
if (strcmp(h_avs, "Y") == 0) risk += 0;
else if (strcmp(h_avs, "A") == 0) risk += 50;
else if (strcmp(h_avs, "Z") == 0) risk += 80;
else if (strcmp(h_avs, "N") == 0) risk += 200;
else risk += 150; /* U 및 기타 */
if (strcmp(h_cvv, "M") == 0) risk += 0;
else if (strcmp(h_cvv, "N") == 0) risk += 400;
else if (strcmp(h_cvv, "P") == 0) risk += 150;
else risk += 150; /* U 및 기타 */
if (strcmp(h_liab, "MERCHANT") == 0) {
risk += 100;
userlog("[%s] 3DS 책임 미전환(MERCHANT) — 위험 가중 pid=%ld", svcname, h_pid);
}
risk += (long)(h_pid % 50); /* 소폭 분산 */
if (risk < 0) risk = 0;
if (risk > 999) risk = 999;
h_score = risk;
/* ===== 4) 판정 분기부 (임계값) ===== */
if (risk > 900) {
strcpy(h_dec, "DECLINE"); rc = 2;
} else if (risk > 750) {
strcpy(h_dec, "REVIEW"); rc = 1;
} else {
strcpy(h_dec, "APPROVE"); rc = 0;
}
userlog("[%s] 리스크 산출 pid=%ld avs=%s cvv=%s liab=%s score=%ld dec=%s",
svcname, h_pid, h_avs, h_cvv, h_liab, h_score, h_dec);
/* ===== 5) 본처리부 (au_verify 채번·INSERT) ===== */
EXEC SQL SELECT nextval('au_verify_seq') INTO :h_vrf;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8520L); Bchg(b, T_MSG, 0, "검증 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO au_verify
(vrf_id, purchase_id, avs_code, cvv_code, risk_score, decision)
VALUES (:h_vrf, :h_pid, :h_avs, :h_cvv, :h_score, :h_dec);
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8521L); Bchg(b, T_MSG, 0, "검증결과 적재 실패", 0L);
userlog("[%s] au_verify 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 6) 적재 재검증부 (기록행 재조회) ===== */
EXEC SQL SELECT decision, risk_score INTO :h_dec, :h_score
FROM au_verify WHERE vrf_id = :h_vrf;
if (sqlca.sqlcode < 0 || sqlca.sqlcode == 100) {
setl(b, T_RC, 8530L); Bchg(b, T_MSG, 0, "검증 적재 재검증 실패", 0L);
userlog("[%s] 재검증 실패 vrf=%ld code=%d", svcname, h_vrf, sqlca.sqlcode); FAIL(b);
}
/* ===== 7) 응답부 ===== */
Bchg(b, T_STATUS, 0, h_dec, 0L); /* decision: APPROVE/REVIEW/DECLINE */
setl(b, T_ARG1, h_score); /* risk_score */
setl(b, T_ID1, h_vrf);
setl(b, T_RC, rc);
Bchg(b, T_MSG, 0,
(rc == 2 ? "리스크 거절" : (rc == 1 ? "리스크 검토대상" : "검증 통과")), 0L);
/* ===== 8) 완료 로깅 ===== */
userlog("[%s] 완료: pid=%ld decision=%s score=%ld vrf=%ld rc=%ld",
svcname, h_pid, h_dec, h_score, h_vrf, rc);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */

24
app/src/au/svc/TOKENIZE.h Normal file
View file

@ -0,0 +1,24 @@
/*
* TOKENIZE.h - au TOKENIZE (copybook / record header).
* : PAN / (PCI PAN /).
* Request fields: T_KEY1( )
* Response fields: T_STR1(token), T_STR2(pan_masked), T_STATUS(NEW/FOUND/NOTFOUND),
* T_ID1( 퀀 audit id), T_RC, T_MSG
*/
#ifndef TOKENIZE_H
#define TOKENIZE_H
#include "au_svc.h"
typedef struct {
char token[40]; /* 표면 토큰 (비민감) */
char pan_masked[40]; /* 4111-****-****-1111 */
char pan_hash[80]; /* SHA-256(PAN) hex — 원문 불가역 */
char bin[12];
char last4[8];
char status[12]; /* NEW / FOUND / NOTFOUND */
} au_tokenize_rec_t;
void TOKENIZE(TPSVCINFO *p);
#endif /* TOKENIZE_H */

143
app/src/au/svc/TOKENIZE.pgc Normal file
View file

@ -0,0 +1,143 @@
/*
* TOKENIZE.pgc - au 모듈 서비스 TOKENIZE (one service = one file).
* PAN 토큰화 조회/발급: 입력 PAN 을 SHA-256 해시로 card_vault 에서 조회하고,
* 있으면 기존 토큰을, 없으면 신규 토큰('TKN-'||md5 앞12)을 발급/적재한다.
* PCI 스코프 축소 규율: 원문 PAN 은 저장하지 않고(마스킹+해시만), 응답에도
* 절대 싣지 않는다. 응답은 토큰과 마스킹 문자열, 발급 상태만 반환한다.
* 처리단계: 입력검증 -> 해시산출 -> 볼트조회 -> 업무규칙분기(원문/마스킹) ->
* 본처리(신규발급 INSERT) -> 적재 재검증 -> 응답 -> 로깅.
* 호출자 XA 브랜치에서 수행(EXEC SQL CONNECT 없음). au_svr 가 tpsvrinit 에서 광고.
* 캐스팅 규율: 호스트변수 date 캐스팅 금지. 해시는 convert_to(...,'UTF8') 로
* bytea 변환하여 시드(sha256(card_no::bytea))와 동일 해시를 얻는다(ASCII 동치).
*/
#include "TOKENIZE.h"
/* TOKENIZE - PAN 토큰화 조회/발급 (원문 PAN 미응답). */
void TOKENIZE(TPSVCINFO *p)
{
UBFH *b = (UBFH *)p->data;
const char *svcname = "TOKENIZE";
/* ---- 지역 스크래치 (DECLARE SECTION 밖에서 초기화) ---- */
long rc = 0, is_pan = 0, i = 0, ndigit = 0, slen = 0;
char inbuf[48];
EXEC SQL BEGIN DECLARE SECTION;
char h_card[48]; /* 입력 원문/마스킹 — 응답에 원문으로 절대 싣지 않음 */
char h_token[48];
char h_masked[48];
char h_hash[80];
long h_found;
long h_tokseq;
EXEC SQL END DECLARE SECTION;
memset(inbuf, 0, sizeof(inbuf));
memset(h_card, 0, sizeof(h_card)); memset(h_token, 0, sizeof(h_token));
memset(h_masked, 0, sizeof(h_masked)); memset(h_hash, 0, sizeof(h_hash));
h_found = 0; h_tokseq = 0;
userlog("[%s] 진입: PAN 토큰화 조회/발급 (원문 미저장/미응답)", svcname);
/* ===== 1) 입력 검증부 ===== */
gets_(b, T_KEY1, inbuf, sizeof(inbuf));
if (inbuf[0] == 0) {
setl(b, T_RC, 8801L); Bchg(b, T_MSG, 0, "카드번호 필수 누락", 0L);
userlog("[%s] 입력검증 실패: 카드번호 누락", svcname); FAIL(b);
}
strncpy(h_card, inbuf, sizeof(h_card) - 1); h_card[sizeof(h_card) - 1] = 0;
slen = (long)strlen(h_card);
if (slen < 4) {
setl(b, T_RC, 8802L); Bchg(b, T_MSG, 0, "카드번호 형식 오류(길이 부족)", 0L);
userlog("[%s] 입력검증 실패: 길이 부족 len=%ld", svcname, slen); FAIL(b);
}
/* ===== 2) 입력 유형 판정부 (원문 PAN vs 마스킹) =====
* 전부 숫자이고 길이 12~19 이면 원문 PAN 으로 보고 신규발급을 허용한다.
* 그 외(대시/별표 포함)는 마스킹 문자열로 보고 조회만 수행한다. */
for (i = 0, ndigit = 0; i < slen; i++) {
if (h_card[i] >= '0' && h_card[i] <= '9') ndigit++;
}
is_pan = (ndigit == slen && slen >= 12 && slen <= 19) ? 1 : 0;
userlog("[%s] 입력유형 is_pan=%ld len=%ld digits=%ld", svcname, is_pan, slen, ndigit);
/* ===== 3) 해시 산출부 (원문 불가역 SHA-256) =====
* 시드(97c)는 sha256(card_no::bytea) 로 적재했다. convert_to(text,'UTF8') 는
* ASCII 숫자열에 대해 동일 bytea 를 주므로 같은 해시로 조회가 성립한다. */
EXEC SQL SELECT encode(sha256(convert_to(:h_card, 'UTF8')), 'hex') INTO :h_hash;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8810L); Bchg(b, T_MSG, 0, "해시 산출 실패", 0L);
userlog("[%s] 해시 산출 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
/* ===== 4) 볼트 조회부 (해시 일치 또는 마스킹 일치) ===== */
EXEC SQL SELECT token, pan_masked INTO :h_token, :h_masked
FROM card_vault
WHERE pan_hash = :h_hash OR pan_masked = :h_card
LIMIT 1;
if (sqlca.sqlcode == 100) {
h_found = 0;
} else if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8811L); Bchg(b, T_MSG, 0, "토큰볼트 조회 오류", 0L);
userlog("[%s] 볼트 조회 오류 [%d]", svcname, sqlca.sqlcode); FAIL(b);
} else {
h_found = 1;
}
/* ===== 5) 업무규칙 분기부 ===== */
if (h_found) {
/* 이미 토큰화된 카드: 기존 토큰 반환 */
strcpy(inbuf, "FOUND"); rc = 0;
userlog("[%s] 기존 토큰 조회 성공 token=%s", svcname, h_token);
} else if (!is_pan) {
/* 마스킹 입력인데 미발견: 원문이 없어 발급 불가 — 조회 미스로 응답 */
strcpy(inbuf, "NOTFOUND"); rc = 1;
strncpy(h_masked, h_card, sizeof(h_masked) - 1); /* 입력이 이미 마스킹(비민감) */
h_masked[sizeof(h_masked) - 1] = 0;
h_token[0] = 0;
userlog("[%s] 마스킹 입력 미발견 — 발급 보류 rc=1", svcname);
} else {
/* ===== 6) 본처리부 (신규 토큰 발급 INSERT) ===== */
strcpy(inbuf, "NEW"); rc = 0;
EXEC SQL SELECT nextval('card_token_seq') INTO :h_tokseq; /* audit 채번 */
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8820L); Bchg(b, T_MSG, 0, "토큰 시퀀스 채번 실패", 0L);
userlog("[%s] 시퀀스 채번 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
EXEC SQL INSERT INTO card_vault
(token, pan_masked, pan_hash, bin, last4)
VALUES ('TKN-' || substr(md5(:h_card), 1, 12),
substr(:h_card, 1, 4) || '-****-****-' || right(:h_card, 4),
:h_hash, substr(:h_card, 1, 6), right(:h_card, 4))
RETURNING token, pan_masked INTO :h_token, :h_masked;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8821L); Bchg(b, T_MSG, 0, "토큰볼트 적재 실패", 0L);
userlog("[%s] 볼트 적재 실패 [%d]", svcname, sqlca.sqlcode); FAIL(b);
}
userlog("[%s] 신규 토큰 발급 seq=%ld token=%s", svcname, h_tokseq, h_token);
}
/* ===== 7) 적재 재검증부 (발급/조회 결과 정합성) ===== */
if (h_token[0]) {
EXEC SQL SELECT count(*) INTO :h_found FROM card_vault WHERE token = :h_token;
if (sqlca.sqlcode < 0) {
setl(b, T_RC, 8830L); Bchg(b, T_MSG, 0, "토큰 재검증 조회 오류", 0L); FAIL(b);
}
if (h_found <= 0) {
setl(b, T_RC, 8831L); Bchg(b, T_MSG, 0, "토큰 재검증 실패(적재행 미확인)", 0L);
userlog("[%s] 재검증 실패 token=%s", svcname, h_token); FAIL(b);
}
}
/* ===== 8) 응답부 (토큰/마스킹/상태만 — 원문 PAN 미포함) ===== */
Bchg(b, T_STR1, 0, h_token, 0L); /* token (비민감) */
Bchg(b, T_STR2, 0, h_masked, 0L); /* pan_masked (비민감) */
Bchg(b, T_STATUS, 0, inbuf, 0L); /* NEW / FOUND / NOTFOUND */
if (h_tokseq > 0) setl(b, T_ID1, h_tokseq);
setl(b, T_RC, rc);
if (rc == 0) Bchg(b, T_MSG, 0, "토큰화 정상", 0L);
/* ===== 9) 완료 로깅 ===== */
userlog("[%s] 완료: status=%s rc=%ld token=%s masked=%s",
svcname, inbuf, rc, h_token, h_masked);
OK(b);
}
/* vim: set ts=4 sw=4 et smartindent: */