goldenChat base source add
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user