견적관리 결재상신 — 외부 커넥션 'Amaranth - 결재' auth_config 자동 복호화
Build and Push Images / build-and-push (push) Has been cancelled

증상:
 - 견적관리에서 결재상신 클릭 시 SSO URL 발급 실패. external_rest_api_connections
   테이블의 accessToken/hashKey 가 AES-256-GCM 으로 암호화되어 저장되는데
   amaranthApprovalClient.loadApprovalConnection() 가 평문 그대로 사용함.

수정:
 - ExternalRestApiConnectionService.encryptSensitiveData/decryptSensitiveData 와
   동일한 알고리즘(aes-256-gcm, scrypt(DB_PASSWORD_SECRET,'salt',32)) 으로 복호화
   하는 tryDecrypt 헬퍼를 amaranthApprovalClient 내부에 추가.
 - loadApprovalConnection 에서 accessToken/hashKey 만 자동 복호화
   (callerName/groupSeq 는 평문 저장이므로 그대로).
 - 'iv:authTag:cipher' 3-part 형식이 아니면 평문으로 간주(마이그레이션 호환).

검증:
 - 복호화 후 accessToken='MN5KzKBWRAa92BPxDlRLl3GcsxeZXc' / hashKey='2251910...'
   로 wace_plm AmaranthApprovalApiClient.java 의 하드코딩 값과 일치 확인.
This commit is contained in:
chpark
2026-05-15 16:12:22 +09:00
parent 30a891f4b8
commit 45cd7db199
@@ -22,6 +22,33 @@ import { logger } from "../utils/logger";
const CONNECTION_NAME = "Amaranth - 결재";
// external_rest_api_connections.auth_config 의 accessToken/hashKey 는
// ExternalRestApiConnectionService 에서 AES-256-GCM 으로 암호화되어 저장됨
// (`iv:authTag:ciphertext` 3-part hex). 같은 키/알고리즘으로 복호화한다.
const ENC_ALGORITHM = "aes-256-gcm";
const ENC_KEY = crypto.scryptSync(
process.env.DB_PASSWORD_SECRET || "default-secret-key-change-in-production",
"salt",
32,
);
function tryDecrypt(value: string): string {
if (!value || typeof value !== "string") return value;
const parts = value.split(":");
if (parts.length !== 3) return value; // 평문 그대로 (마이그레이션 호환)
try {
const iv = Buffer.from(parts[0], "hex");
const authTag = Buffer.from(parts[1], "hex");
const decipher = crypto.createDecipheriv(ENC_ALGORITHM, ENC_KEY, iv) as crypto.DecipherGCM;
decipher.setAuthTag(authTag);
let out = decipher.update(parts[2], "hex", "utf8");
out += decipher.final("utf8");
return out;
} catch {
return value; // 복호화 실패 시 원본 (평문 가정)
}
}
interface AmaranthAuth {
baseUrl: string;
callerName: string;
@@ -31,7 +58,7 @@ interface AmaranthAuth {
aesKey: string;
}
/** DB 의 외부 커넥션 'Amaranth - 결재' 에서 인증 정보를 로드 */
/** DB 의 외부 커넥션 'Amaranth - 결재' 에서 인증 정보를 로드 (민감 필드 자동 복호화) */
export async function loadApprovalConnection(): Promise<AmaranthAuth> {
const row = await queryOne<any>(
`SELECT base_url, auth_config FROM external_rest_api_connections
@@ -48,10 +75,10 @@ export async function loadApprovalConnection(): Promise<AmaranthAuth> {
}
return {
baseUrl: String(row.base_url || "").replace(/\/+$/, ""),
callerName: cfg.callerName,
accessToken: cfg.accessToken,
hashKey: cfg.hashKey,
groupSeq: cfg.groupSeq,
callerName: cfg.callerName, // 평문 (암호화 대상 아님)
accessToken: tryDecrypt(cfg.accessToken),
hashKey: tryDecrypt(cfg.hashKey),
groupSeq: cfg.groupSeq, // 평문
aesKey: cfg.aesKey || "8441e27489d402cd",
};
}