feat: 암호화모듈 프레임워크 적용 및 사이트 특화 전략 추가

- 서브모듈 포인터 업데이트 (elink-online-common, elink-online-core-jpa)
- HsmContextSha256KeyDerivationStrategy: SHA-256(masterKey || contextValue) 키 도출 전략
  elink-online-common에서 eapim-online custom 패키지로 분리 (사이트 의존성)
  FQCN: com.eactive.eai.custom.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy
- HsmContextSha256KeyDerivationStrategyTest 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-05-06 16:50:17 +09:00
parent efcc7fe208
commit c225104711
4 changed files with 249 additions and 2 deletions
@@ -0,0 +1,68 @@
package com.eactive.eai.custom.security.keyderiv.strategy;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import javax.crypto.SecretKey;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.security.keyderiv.KeyDerivationStrategy;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* HSM 마스터키 + 런타임 컨텍스트값을 연결(concatenate)한 뒤 SHA-256 해싱으로 키를 도출하는 전략.
* SHA-256 출력은 항상 32바이트(AES-256)이므로 별도 keyLength 파라미터가 불필요하다.
*
* key_deriv_params (JSON) 예시:
* {
* "hsmKeyAlias" : "MASTER_KEY_AES",
* "contextKey" : "X-Api-Group-Seq" -- runtimeContext에서 꺼낼 키 이름
* }
*/
public class HsmContextSha256KeyDerivationStrategy implements KeyDerivationStrategy {
/** DB key_deriv_strategy 컬럼에 사용하는 FQCN 참조용 상수. */
public static final String STRATEGY_CLASS = HsmContextSha256KeyDerivationStrategy.class.getName();
private static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias";
private static final String PARAM_CONTEXT_KEY = "contextKey";
@Override
public DerivedKey deriveKey(Map<String, String> params, Map<String, String> runtimeContext) throws Exception {
String hsmKeyAlias = required(params, PARAM_HSM_KEY_ALIAS);
String contextKey = required(params, PARAM_CONTEXT_KEY);
SecretKey masterKey = hsmCryptoService().getSecretKey(hsmKeyAlias);
byte[] masterKeyBytes = masterKey.getEncoded();
byte[] contextBytes = runtimeContext.getOrDefault(contextKey, "")
.getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[masterKeyBytes.length + contextBytes.length];
System.arraycopy(masterKeyBytes, 0, combined, 0, masterKeyBytes.length);
System.arraycopy(contextBytes, 0, combined, masterKeyBytes.length, contextBytes.length);
byte[] derived = MessageDigest.getInstance("SHA-256").digest(combined);
return new DerivedKey(derived, derived);
}
@Override
public String buildCacheKey(String cryptoName, Map<String, String> params, Map<String, String> runtimeContext) {
String contextKey = params.getOrDefault(PARAM_CONTEXT_KEY, "");
String contextValue = runtimeContext.getOrDefault(contextKey, "");
return cryptoName + ":" + contextValue;
}
private String required(Map<String, String> params, String key) {
String value = params.get(key);
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("key_deriv_params 필수값 누락: " + key);
}
return value;
}
private HsmCryptoService hsmCryptoService() {
return ApplicationContextProvider.getContext().getBean(HsmCryptoService.class);
}
}
@@ -0,0 +1,179 @@
package com.eactive.eai.custom.security.keyderiv;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.hsm.HsmCryptoService;
import com.eactive.eai.common.security.keyderiv.DerivedKey;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.custom.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy;
/**
* HsmContextSha256KeyDerivationStrategy 단위 테스트
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class HsmContextSha256KeyDerivationStrategyTest {
private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES";
private static final byte[] MASTER_KEY = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
private static GenericApplicationContext ctx;
private static HsmCryptoService mockHsmCryptoService;
private static HsmContextSha256KeyDerivationStrategy strategy;
private Map<String, String> params;
private Map<String, String> runtimeContext;
@BeforeAll
static void setUpClass() throws Exception {
mockHsmCryptoService = mock(HsmCryptoService.class);
SecretKey mockSecretKey = new SecretKeySpec(MASTER_KEY, "AES");
when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)).thenReturn(mockSecretKey);
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
strategy = new HsmContextSha256KeyDerivationStrategy();
}
@BeforeEach
void setUpParams() {
params = new HashMap<>();
params.put("hsmKeyAlias", HSM_KEY_ALIAS);
params.put("contextKey", "X-Api-Group-Seq");
runtimeContext = new HashMap<>();
runtimeContext.put("X-Api-Group-Seq", "GROUP_001");
}
// =========================================================================
// 1. deriveKey
// =========================================================================
@Test
@DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, 길이 32바이트(SHA-256 고정 출력)")
void testDeriveKey_validParams_returns32ByteKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertNotNull(dk);
assertNotNull(dk.getEncKey());
assertNotNull(dk.getDecKey());
assertEquals(32, dk.getEncKey().length, "SHA-256 출력은 항상 32바이트여야 한다");
}
@Test
@DisplayName("1-2. SHA-256(masterKey || contextValue) 계산 결과 검증")
void testDeriveKey_sha256ResultIsCorrect() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
byte[] contextBytes = "GROUP_001".getBytes(StandardCharsets.UTF_8);
byte[] combined = new byte[MASTER_KEY.length + contextBytes.length];
System.arraycopy(MASTER_KEY, 0, combined, 0, MASTER_KEY.length);
System.arraycopy(contextBytes, 0, combined, MASTER_KEY.length, contextBytes.length);
byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);
assertArrayEquals(expected, dk.getEncKey(), "SHA-256 해싱 결과가 일치해야 한다");
}
@Test
@DisplayName("1-3. 대칭 알고리즘 — encKey == decKey")
void testDeriveKey_encKeyEqualsDecKey() throws Exception {
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
assertArrayEquals(dk.getEncKey(), dk.getDecKey(), "대칭 방식이므로 encKey와 decKey가 동일해야 한다");
}
@Test
@DisplayName("1-4. runtimeContext 값 없음 — 빈 문자열로 처리")
void testDeriveKey_missingContextValue_emptyStringUsed() throws Exception {
runtimeContext.remove("X-Api-Group-Seq");
DerivedKey dk = strategy.deriveKey(params, runtimeContext);
byte[] expected = MessageDigest.getInstance("SHA-256").digest(MASTER_KEY);
assertArrayEquals(expected, dk.getEncKey(), "컨텍스트 값 없으면 masterKey만 해싱해야 한다");
}
@Test
@DisplayName("1-5. 다른 runtimeContext 값 — 다른 DerivedKey 생성")
void testDeriveKey_differentContextValue_differentKey() throws Exception {
DerivedKey dk1 = strategy.deriveKey(params, runtimeContext);
runtimeContext.put("X-Api-Group-Seq", "GROUP_002");
DerivedKey dk2 = strategy.deriveKey(params, runtimeContext);
assertFalse(java.util.Arrays.equals(dk1.getEncKey(), dk2.getEncKey()),
"컨텍스트 값이 다르면 도출된 키도 달라야 한다");
}
@Test
@DisplayName("1-6. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException")
void testDeriveKey_missingHsmKeyAlias_throwsException() {
params.remove("hsmKeyAlias");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext),
"hsmKeyAlias 누락 시 IllegalArgumentException이 발생해야 한다");
}
@Test
@DisplayName("1-7. 필수 파라미터 누락(contextKey) — IllegalArgumentException")
void testDeriveKey_missingContextKey_throwsException() {
params.remove("contextKey");
assertThrows(IllegalArgumentException.class,
() -> strategy.deriveKey(params, runtimeContext),
"contextKey 누락 시 IllegalArgumentException이 발생해야 한다");
}
// =========================================================================
// 2. buildCacheKey
// =========================================================================
@Test
@DisplayName("2-1. buildCacheKey — cryptoName:contextValue 형식")
void testBuildCacheKey_format() {
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
assertEquals("CRYPTO_A:GROUP_001", cacheKey);
}
@Test
@DisplayName("2-2. buildCacheKey — contextKey 값 없으면 빈 문자열")
void testBuildCacheKey_missingContextValue_emptyString() {
runtimeContext.remove("X-Api-Group-Seq");
String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
assertEquals("CRYPTO_A:", cacheKey);
}
@Test
@DisplayName("2-3. buildCacheKey — 다른 cryptoName은 다른 캐시 키")
void testBuildCacheKey_differentCryptoName_differentKey() {
String key1 = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext);
String key2 = strategy.buildCacheKey("CRYPTO_B", params, runtimeContext);
assertNotEquals(key1, key2);
}
}