diff --git a/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java b/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java index 27625b5..bb2286b 100644 --- a/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java +++ b/src/main/java/com/eactive/eai/common/security/CryptoModuleManager.java @@ -144,8 +144,12 @@ public class CryptoModuleManager implements Lifecycle { dynamicKeyCache.put(cacheKey, new CachedDerivedKey(derivedKey, ttl)); } } - - byte[] iv = vo.getIvHex() != null ? DatatypeConverter.parseHexBinary(vo.getIvHex()) : null; + byte[] iv = null; + if(derivedKey.getIv() != null) { + iv = derivedKey.getIv(); + } else if (vo.getIvHex() != null){ + iv = DatatypeConverter.parseHexBinary(vo.getIvHex()); + } return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey()); } diff --git a/src/main/java/com/eactive/eai/common/security/keyderiv/DerivedKey.java b/src/main/java/com/eactive/eai/common/security/keyderiv/DerivedKey.java index 93e22b4..5d09cc0 100644 --- a/src/main/java/com/eactive/eai/common/security/keyderiv/DerivedKey.java +++ b/src/main/java/com/eactive/eai/common/security/keyderiv/DerivedKey.java @@ -4,10 +4,19 @@ public class DerivedKey { private final byte[] encKey; private final byte[] decKey; + private final byte[] iv; + public DerivedKey(byte[] encKey, byte[] decKey) { this.encKey = encKey; this.decKey = decKey != null ? decKey : encKey; + this.iv = null; + } + + public DerivedKey(byte[] encKey, byte[] decKey, byte[] iv) { + this.encKey = encKey; + this.decKey = decKey != null ? decKey : encKey; + this.iv = iv; } public byte[] getEncKey() { @@ -17,4 +26,8 @@ public class DerivedKey { public byte[] getDecKey() { return decKey; } + + public byte[] getIv() { + return iv; + } } diff --git a/src/main/java/com/eactive/eai/common/security/keyderiv/KeyDerivationStrategy.java b/src/main/java/com/eactive/eai/common/security/keyderiv/KeyDerivationStrategy.java index e6914ad..ad3b708 100644 --- a/src/main/java/com/eactive/eai/common/security/keyderiv/KeyDerivationStrategy.java +++ b/src/main/java/com/eactive/eai/common/security/keyderiv/KeyDerivationStrategy.java @@ -5,7 +5,8 @@ import java.util.Map; public interface KeyDerivationStrategy { public static final String PARAM_HSM_KEY_ALIAS = "hsmKeyAlias"; - public static final String PARAM_CONTEXT_KEY = "contextKey"; + public static final String PARAM_HSM_IV_ALIAS = "hsmIvAlias"; + public static final String PARAM_CONTEXT_KEY = "contextKey"; /** diff --git a/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/BaseHsmKeyDerivationStrategy.java b/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/BaseHsmKeyDerivationStrategy.java index 7de4ab2..be37689 100644 --- a/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/BaseHsmKeyDerivationStrategy.java +++ b/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/BaseHsmKeyDerivationStrategy.java @@ -24,6 +24,11 @@ public abstract class BaseHsmKeyDerivationStrategy implements KeyDerivationStrat return masterKey.getEncoded(); } + protected byte[] getHsmIv(Map params) throws HsmException { + String hsmKeyAlias = required(params, PARAM_HSM_IV_ALIAS); + return getHsmKey(hsmKeyAlias); + } + private String required(Map params, String key) { String value = params.get(key); if (value == null || value.trim().isEmpty()) { diff --git a/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmKeyAndIvDerivationStrategy.java b/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmKeyAndIvDerivationStrategy.java new file mode 100644 index 0000000..a4714dc --- /dev/null +++ b/src/main/java/com/eactive/eai/common/security/keyderiv/strategy/HsmKeyAndIvDerivationStrategy.java @@ -0,0 +1,25 @@ +package com.eactive.eai.common.security.keyderiv.strategy; + +import java.util.Map; + +import com.eactive.eai.common.security.keyderiv.DerivedKey; + +/** + * HSM 마스터키를 사용하는 전략 + * + * key_deriv_params (JSON) 예시: { "hsmKeyAlias" : "MASTER_KEY_AES", "hsmIvAlias" : "MASTER_IV_AES"} + */ +public class HsmKeyAndIvDerivationStrategy extends BaseHsmKeyDerivationStrategy { + + @Override + public DerivedKey deriveKey(Map params, Map runtimeContext) throws Exception { + byte[] masterKeyBytes = super.getHsmKey(params); + byte[] iv = super.getHsmIv(params); + return new DerivedKey(masterKeyBytes, masterKeyBytes, iv); + } + + @Override + public String buildCacheKey(String cryptoName, Map params, Map runtimeContext) { + return cryptoName; + } +} diff --git a/src/test/java/com/eactive/eai/common/security/keyderiv/DerivedKeyTest.java b/src/test/java/com/eactive/eai/common/security/keyderiv/DerivedKeyTest.java index 61098eb..5e2babc 100644 --- a/src/test/java/com/eactive/eai/common/security/keyderiv/DerivedKeyTest.java +++ b/src/test/java/com/eactive/eai/common/security/keyderiv/DerivedKeyTest.java @@ -36,4 +36,29 @@ class DerivedKeyTest { assertSame(dk.getEncKey(), dk.getDecKey()); } + + @Test + @DisplayName("1-4. 2-arg 생성자 — iv는 null") + void testTwoArgConstructor_ivIsNull() { + DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2}); + + assertNull(dk.getIv(), "2-arg 생성자로 생성 시 iv는 null이어야 한다"); + } + + @Test + @DisplayName("1-5. 3-arg 생성자 — iv 반환") + void testThreeArgConstructor_ivReturned() { + byte[] iv = {0x01, 0x02, 0x03}; + DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2}, iv); + + assertArrayEquals(iv, dk.getIv()); + } + + @Test + @DisplayName("1-6. 3-arg 생성자 — iv null 허용") + void testThreeArgConstructor_nullIv() { + DerivedKey dk = new DerivedKey(new byte[]{1}, new byte[]{2}, null); + + assertNull(dk.getIv()); + } } diff --git a/src/test/java/com/eactive/eai/common/security/keyderiv/HsmKeyAndIvDerivationStrategyTest.java b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmKeyAndIvDerivationStrategyTest.java new file mode 100644 index 0000000..f879c23 --- /dev/null +++ b/src/test/java/com/eactive/eai/common/security/keyderiv/HsmKeyAndIvDerivationStrategyTest.java @@ -0,0 +1,149 @@ +package com.eactive.eai.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 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.strategy.HsmKeyAndIvDerivationStrategy; +import com.eactive.eai.common.util.ApplicationContextProvider; + +/** + * HsmKeyAndIvDerivationStrategy 단위 테스트 + */ +@TestMethodOrder(MethodOrderer.DisplayName.class) +class HsmKeyAndIvDerivationStrategyTest { + + private static final String HSM_KEY_ALIAS = "MASTER_KEY_AES"; + private static final String HSM_IV_ALIAS = "MASTER_IV_AES"; + private static final byte[] MASTER_KEY = new byte[]{0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08, + 0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10}; + private static final byte[] MASTER_IV = new byte[]{0x10,0x20,0x30,0x40,0x50,0x60,0x70,(byte)0x80, + 0x11,0x21,0x31,0x41,0x51,0x61,0x71,(byte)0x81}; + + private static GenericApplicationContext ctx; + private static HsmCryptoService mockHsmCryptoService; + private static HsmKeyAndIvDerivationStrategy 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 HsmKeyAndIvDerivationStrategy(); + } + + @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 반환, key와 iv 모두 non-null") + void testDeriveKey_validParams_returnsKeyAndIv() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertNotNull(dk); + assertNotNull(dk.getEncKey(), "encKey는 null이 아니어야 한다"); + assertNotNull(dk.getDecKey(), "decKey는 null이 아니어야 한다"); + assertNotNull(dk.getIv(), "iv는 null이 아니어야 한다"); + } + + @Test + @DisplayName("1-2. encKey == decKey (대칭 방식)") + void testDeriveKey_encKeyEqualsDecKey() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertArrayEquals(dk.getEncKey(), dk.getDecKey()); + } + + @Test + @DisplayName("1-3. encKey는 hsmKeyAlias에 해당하는 HSM 키") + void testDeriveKey_encKeyMatchesMasterKey() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertArrayEquals(MASTER_KEY, dk.getEncKey(), "encKey는 hsmKeyAlias로 가져온 마스터키여야 한다"); + } + + @Test + @DisplayName("1-4. iv는 hsmIvAlias에 해당하는 HSM 키") + void testDeriveKey_ivMatchesMasterIv() throws Exception { + DerivedKey dk = strategy.deriveKey(params, runtimeContext); + + assertArrayEquals(MASTER_IV, dk.getIv(), "iv는 hsmIvAlias로 가져온 HSM 값이어야 한다"); + } + + @Test + @DisplayName("1-5. 필수 파라미터 누락(hsmKeyAlias) — IllegalArgumentException") + void testDeriveKey_missingHsmKeyAlias_throwsException() { + params.remove("hsmKeyAlias"); + + assertThrows(IllegalArgumentException.class, + () -> strategy.deriveKey(params, runtimeContext)); + } + + @Test + @DisplayName("1-6. 필수 파라미터 누락(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() { + String cacheKey = strategy.buildCacheKey("CRYPTO_A", params, runtimeContext); + + assertEquals("CRYPTO_A", cacheKey); + } + + @Test + @DisplayName("2-2. 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); + } +}