From c74d1777dfefcaab5767c76bc63dd246c729ae5a Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 27 May 2026 13:48:23 +0900 Subject: [PATCH] =?UTF-8?q?Dynamic=20IV=20=EB=8F=84=EC=B6=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=E2=80=94=20DerivedKey=20iv=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20HsmKeyAndIvDerivationStrategy=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- elink-online-common | 2 +- .../HsmKeyAndIvSliceDerivationStrategy.java | 41 ++++ ...smKeyAndIvSliceDerivationStrategyTest.java | 175 ++++++++++++++++++ 3 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategy.java create mode 100644 src/test/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategyTest.java diff --git a/elink-online-common b/elink-online-common index 29a486b..6809bf6 160000 --- a/elink-online-common +++ b/elink-online-common @@ -1 +1 @@ -Subproject commit 29a486bf9babbb02594c3c1bb67eae36f588e904 +Subproject commit 6809bf66b85f9473ecfd1a85edddab1b896fbb18 diff --git a/src/main/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategy.java b/src/main/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategy.java new file mode 100644 index 0000000..09b1dca --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategy.java @@ -0,0 +1,41 @@ +package com.eactive.eai.custom.common.security.keyderiv; + +import java.util.Map; + +import javax.xml.bind.DatatypeConverter; + +import com.eactive.eai.common.security.keyderiv.DerivedKey; +import com.eactive.eai.common.security.keyderiv.strategy.BaseHsmKeyDerivationStrategy; + +/** + * HSM에서 키와 IV를 각각 가져오는 전략. + * IV는 HSM에서 가져온 바이트 배열을 hex 문자열로 변환한 뒤 substring(32, 64)를 적용하여 사용한다. + * (32바이트 HSM 키 → 64자 hex → substring(32,64) → 16바이트 IV) + * + * key_deriv_params (JSON) 예시: + * { "hsmKeyAlias" : "MASTER_KEY_AES", "hsmIvAlias" : "MASTER_IV_AES" } + */ +public class HsmKeyAndIvSliceDerivationStrategy extends BaseHsmKeyDerivationStrategy { + + private static final int IV_HEX_OFFSET = 32; + private static final int IV_HEX_END = 64; + + @Override + public DerivedKey deriveKey(Map params, Map runtimeContext) throws Exception { + byte[] keyBytes = super.getHsmKey(params); + byte[] rawIv = super.getHsmIv(params); + String ivHex = DatatypeConverter.printHexBinary(rawIv); + if (ivHex.length() < IV_HEX_END) { + throw new IllegalArgumentException( + "hsmIvAlias hex 길이 부족: 최소 " + IV_HEX_END + "자 필요, 실제=" + ivHex.length() + + " (" + rawIv.length + "바이트)"); + } + byte[] iv = DatatypeConverter.parseHexBinary(ivHex.substring(IV_HEX_OFFSET, IV_HEX_END)); + return new DerivedKey(keyBytes, keyBytes, iv); + } + + @Override + public String buildCacheKey(String cryptoName, Map params, Map runtimeContext) { + return cryptoName; + } +} diff --git a/src/test/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategyTest.java b/src/test/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategyTest.java new file mode 100644 index 0000000..4edcef1 --- /dev/null +++ b/src/test/java/com/eactive/eai/custom/common/security/keyderiv/HsmKeyAndIvSliceDerivationStrategyTest.java @@ -0,0 +1,175 @@ +package com.eactive.eai.custom.common.security.keyderiv; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; + +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; + +/** + * HsmKeyAndIvSliceDerivationStrategy 단위 테스트 + */ +@TestMethodOrder(MethodOrderer.DisplayName.class) +class HsmKeyAndIvSliceDerivationStrategyTest { + + private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES"; + private static final String HSM_IV_ALIAS = "MASTER_IV_AES"; + + /** 32바이트 암호화 키 (AES-256) */ + private static final byte[] MASTER_KEY = new byte[32]; + /** 32바이트 IV 원본 → hex 64자 → substring(32,64) → 16바이트 IV */ + private static final byte[] MASTER_IV = new byte[32]; + + static { + for (int i = 0; i < MASTER_KEY.length; i++) MASTER_KEY[i] = (byte) (i + 1); + for (int i = 0; i < MASTER_IV.length; i++) MASTER_IV[i] = (byte) (i + 0x10); + } + + private static GenericApplicationContext ctx; + private static HsmCryptoService mockHsmCryptoService; + private static HsmKeyAndIvSliceDerivationStrategy strategy; + + private Map params; + private Map runtimeContext; + + @BeforeAll + static void setUpClass() throws Exception { + mockHsmCryptoService = mock(HsmCryptoService.class); + when(mockHsmCryptoService.getSecretKey(HSM_KEY_ALIAS)) + .thenReturn(new SecretKeySpec(MASTER_KEY, "AES")); + when(mockHsmCryptoService.getSecretKey(HSM_IV_ALIAS)) + .thenReturn(new SecretKeySpec(MASTER_IV, "AES")); + + ctx = new GenericApplicationContext(); + ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService); + ctx.registerBeanDefinition("applicationContextProvider", + BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class) + .getBeanDefinition()); + ctx.refresh(); + + strategy = new HsmKeyAndIvSliceDerivationStrategy(); + } + + @BeforeEach + void setUpParams() { + params = new HashMap<>(); + params.put("hsmKeyAlias", HSM_KEY_ALIAS); + params.put("hsmIvAlias", HSM_IV_ALIAS); + + runtimeContext = new HashMap<>(); + } + + // ========================================================================= + // 1. deriveKey + // ========================================================================= + + @Test + @DisplayName("1-1. 정상 파라미터 — DerivedKey 반환, encKey/iv 모두 non-null") + void testDeriveKey_validParams_returnsKeyAndIv() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertNotNull(dk); + assertNotNull(dk.getEncKey()); + assertNotNull(dk.getIv()); + } + + @Test + @DisplayName("1-2. encKey는 hsmKeyAlias에 해당하는 HSM 키") + void testDeriveKey_encKeyMatchesMasterKey() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertArrayEquals(MASTER_KEY, dk.getEncKey()); + } + + @Test + @DisplayName("1-3. encKey == decKey (대칭 방식)") + void testDeriveKey_encKeyEqualsDecKey() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertArrayEquals(dk.getEncKey(), dk.getDecKey()); + } + + @Test + @DisplayName("1-4. iv는 HSM IV hex 문자열의 substring(32,64)와 일치") + void testDeriveKey_ivIsHexSubstring32To64() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + String ivHex = DatatypeConverter.printHexBinary(MASTER_IV); + byte[] expected = DatatypeConverter.parseHexBinary(ivHex.substring(32, 64)); + assertArrayEquals(expected, dk.getIv(), "iv는 HSM IV hex substring(32,64) 파싱 결과여야 한다"); + } + + @Test + @DisplayName("1-5. iv 길이 == 16바이트 (hex 32자 = 16바이트)") + void testDeriveKey_ivLength16() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertEquals(16, dk.getIv().length); + } + + @Test + @DisplayName("1-6. hsmIvAlias hex 길이가 64자 미만 — IllegalArgumentException") + void testDeriveKey_ivTooShort_throwsException() throws Exception { + byte[] shortIv = new byte[15]; // hex 30자 → 64자 미만 + when(mockHsmCryptoService.getSecretKey("SHORT_IV")) + .thenReturn(new SecretKeySpec(shortIv, "AES")); + params.put("hsmIvAlias", "SHORT_IV"); + + assertThrows(IllegalArgumentException.class, + () -> strategy.deriveKey(params, runtimeContext), + "IV hex 길이 부족 시 IllegalArgumentException이 발생해야 한다"); + } + + @Test + @DisplayName("1-7. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException") + void testDeriveKey_missingHsmKeyAlias_throwsException() { + params.remove("hsmKeyAlias"); + + assertThrows(IllegalArgumentException.class, + () -> strategy.deriveKey(params, runtimeContext)); + } + + @Test + @DisplayName("1-8. 필수 파라미터 누락(hsmIvAlias) — IllegalArgumentException") + void testDeriveKey_missingHsmIvAlias_throwsException() { + params.remove("hsmIvAlias"); + + assertThrows(IllegalArgumentException.class, + () -> strategy.deriveKey(params, runtimeContext)); + } + + // ========================================================================= + // 2. buildCacheKey + // ========================================================================= + + @Test + @DisplayName("2-1. buildCacheKey — cryptoName 단독 반환") + void testBuildCacheKey_returnsCryptoName() { + assertEquals("CRYPTO_A", strategy.buildCacheKey("CRYPTO_A", params, runtimeContext)); + } + + @Test + @DisplayName("2-2. buildCacheKey — 다른 cryptoName은 다른 캐시 키") + void testBuildCacheKey_differentCryptoName_differentKey() { + assertNotEquals( + strategy.buildCacheKey("CRYPTO_A", params, runtimeContext), + strategy.buildCacheKey("CRYPTO_B", params, runtimeContext)); + } +}