From 085dd024d8fb4ce874fb3a069ccbbfb81ac86c7e Mon Sep 17 00:00:00 2001 From: curry772 Date: Tue, 12 May 2026 16:37:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20HsmContextSha256KeyDerivationStrategyTe?= =?UTF-8?q?st=20common=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ...ontextSha256KeyDerivationStrategyTest.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java diff --git a/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java new file mode 100644 index 0000000..b16da5c --- /dev/null +++ b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmContextSha256KeyDerivationStrategyTest.java @@ -0,0 +1,179 @@ +package com.eactive.eai.common.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.security.keyderiv.strategy.HsmContextSha256KeyDerivationStrategy; +import com.eactive.eai.common.util.ApplicationContextProvider; + +/** + * 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 params; + private Map 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); + } +}