goldenChat base source add

This commit is contained in:
aidev
2026-05-23 15:11:48 +09:00
commit a4ea7762b5
2081 changed files with 1155760 additions and 0 deletions
@@ -0,0 +1,112 @@
package com.goldenchart.security;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 업비트 API 키 등 민감 문자열 DB 저장용 AES-256-GCM 암호화.
* 평문은 API 응답·로그에 노출하지 않는다.
*/
@Service
@Slf4j
public class SecretCryptoService {
private static final String PREFIX = "ENC:v1:";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
@Value("${goldenchart.secrets.encryption-key:}")
private String encryptionKeyConfig;
private SecretKey secretKey;
private final SecureRandom secureRandom = new SecureRandom();
@PostConstruct
void init() {
String raw = encryptionKeyConfig != null ? encryptionKeyConfig.trim() : "";
if (raw.isEmpty()) {
raw = "goldenchart-dev-change-in-production";
log.warn("[SecretCrypto] GC_SECRETS_ENCRYPTION_KEY 미설정 — 개발용 기본 키 사용 (운영 환경에서는 반드시 설정)");
}
try {
byte[] keyBytes = MessageDigest.getInstance("SHA-256")
.digest(raw.getBytes(StandardCharsets.UTF_8));
this.secretKey = new SecretKeySpec(keyBytes, "AES");
} catch (Exception e) {
throw new IllegalStateException("암호화 키 초기화 실패", e);
}
}
public boolean isEncrypted(String value) {
return value != null && value.startsWith(PREFIX);
}
/** DB 저장용 암호화 (이미 암호문이면 그대로 반환) */
public String encrypt(String plaintext) {
if (plaintext == null || plaintext.isBlank()) {
return plaintext;
}
if (isEncrypted(plaintext)) {
return plaintext;
}
try {
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
ByteBuffer buf = ByteBuffer.allocate(iv.length + encrypted.length);
buf.put(iv);
buf.put(encrypted);
return PREFIX + Base64.getEncoder().encodeToString(buf.array());
} catch (Exception e) {
throw new IllegalStateException("민감 정보 암호화 실패", e);
}
}
/** DB에서 읽은 값 복호화 (레거시 평문은 그대로 반환) */
public String decrypt(String stored) {
if (stored == null || stored.isBlank()) {
return stored;
}
if (!isEncrypted(stored)) {
return stored;
}
try {
String payload = stored.substring(PREFIX.length());
byte[] decoded = Base64.getDecoder().decode(payload);
ByteBuffer buf = ByteBuffer.wrap(decoded);
byte[] iv = new byte[GCM_IV_LENGTH];
buf.get(iv);
byte[] ciphertext = new byte[buf.remaining()];
buf.get(ciphertext);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] plain = cipher.doFinal(ciphertext);
return new String(plain, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("[SecretCrypto] 복호화 실패 — 키 손상 또는 encryption-key 불일치 가능");
throw new IllegalStateException("민감 정보 복호화 실패", e);
}
}
/** API 응답용 — 마지막 4자만 노출 */
public static String maskForDisplay(String plain) {
if (plain == null || plain.length() < 4) {
return null;
}
return "····" + plain.substring(plain.length() - 4);
}
}