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); } } }