Files
myListBridgeAPI/src/main/java/com/global/util/EncryptionUtil.java
2025-11-28 15:34:58 +09:00

169 lines
6.0 KiB
Java

package com.global.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
/**
* CryptoJS 호환 AES 암호화/복호화 유틸리티
* CryptoJS의 OpenSSL 호환 포맷(Salted__)을 지원합니다.
*/
@Slf4j
@Component
public class EncryptionUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private static final String SALTED_PREFIX = "Salted__";
private static final int SALT_LENGTH = 8;
private static final int KEY_LENGTH = 32; // 256 bits
private static final int IV_LENGTH = 16; // 128 bits
@Value("${vibe.encryption.secret-key:MyListBridgeSecretKey1234567}")
private String secretKeyString;
/**
* CryptoJS 호환 복호화
* CryptoJS의 OpenSSL 포맷(Salted__ + salt + data)을 해석합니다.
*
* @param encryptedText Base64 인코딩된 CryptoJS 암호문
* @return 복호화된 평문
*/
public String decrypt(String encryptedText) {
if (encryptedText == null || encryptedText.isEmpty()) {
return null;
}
try {
// 1. Base64 디코딩
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
// 2. "Salted__" 접두사 확인
byte[] saltedPrefix = Arrays.copyOfRange(encryptedData, 0, 8);
String prefix = new String(saltedPrefix, StandardCharsets.UTF_8);
if (!SALTED_PREFIX.equals(prefix)) {
log.error("Invalid CryptoJS format - missing 'Salted__' prefix");
throw new RuntimeException("Invalid CryptoJS format");
}
// 3. Salt 추출 (8바이트)
byte[] salt = Arrays.copyOfRange(encryptedData, 8, 16);
// 4. 실제 암호화된 데이터
byte[] cipherText = Arrays.copyOfRange(encryptedData, 16, encryptedData.length);
// 5. CryptoJS의 EVP_BytesToKey 방식으로 키와 IV 생성
byte[][] keyAndIV = deriveKeyAndIV(secretKeyString.getBytes(StandardCharsets.UTF_8), salt);
byte[] key = keyAndIV[0];
byte[] iv = keyAndIV[1];
// 6. 복호화
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
byte[] decryptedBytes = cipher.doFinal(cipherText);
String result = new String(decryptedBytes, StandardCharsets.UTF_8);
log.debug("복호화 성공");
return result;
} catch (Exception e) {
log.error("CryptoJS 복호화 실패", e);
throw new RuntimeException("복호화 실패", e);
}
}
/**
* CryptoJS/OpenSSL의 EVP_BytesToKey 알고리즘 구현
* 패스워드와 Salt로부터 Key와 IV를 유도합니다.
*
* @param password 패스워드 바이트
* @param salt Salt 바이트
* @return [key, iv] 배열
*/
private byte[][] deriveKeyAndIV(byte[] password, byte[] salt) throws Exception {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] key = new byte[KEY_LENGTH];
byte[] iv = new byte[IV_LENGTH];
byte[] concatenated = new byte[0];
byte[] result = new byte[0];
int totalLength = KEY_LENGTH + IV_LENGTH;
while (result.length < totalLength) {
md5.reset();
md5.update(concatenated);
md5.update(password);
md5.update(salt);
concatenated = md5.digest();
byte[] temp = new byte[result.length + concatenated.length];
System.arraycopy(result, 0, temp, 0, result.length);
System.arraycopy(concatenated, 0, temp, result.length, concatenated.length);
result = temp;
}
System.arraycopy(result, 0, key, 0, KEY_LENGTH);
System.arraycopy(result, KEY_LENGTH, iv, 0, IV_LENGTH);
return new byte[][]{key, iv};
}
/**
* CryptoJS 호환 암호화 (서버에서 암호화할 필요가 있는 경우)
*
* @param plainText 평문
* @return Base64 인코딩된 CryptoJS 포맷 암호문
*/
public String encrypt(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return null;
}
try {
// 1. 랜덤 Salt 생성
byte[] salt = new byte[SALT_LENGTH];
new java.security.SecureRandom().nextBytes(salt);
// 2. Key와 IV 유도
byte[][] keyAndIV = deriveKeyAndIV(secretKeyString.getBytes(StandardCharsets.UTF_8), salt);
byte[] key = keyAndIV[0];
byte[] iv = keyAndIV[1];
// 3. 암호화
SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 4. "Salted__" + salt + encryptedData 조합
byte[] result = new byte[16 + encryptedBytes.length];
System.arraycopy(SALTED_PREFIX.getBytes(StandardCharsets.UTF_8), 0, result, 0, 8);
System.arraycopy(salt, 0, result, 8, 8);
System.arraycopy(encryptedBytes, 0, result, 16, encryptedBytes.length);
// 5. Base64 인코딩
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
log.error("CryptoJS 암호화 실패", e);
throw new RuntimeException("암호화 실패", e);
}
}
}