diff --git a/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpDecryt.java b/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpDecryt.java new file mode 100644 index 0000000..0b2d351 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpDecryt.java @@ -0,0 +1,52 @@ +package com.eactive.eai.custom.transformer.function; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +import com.eactive.eai.common.security.CryptoModuleService; + +/** + * JEP 트랜스포머 함수 — DJB ERP 연동 복호화. + * + * 사용법: DJBErpDecryt(encBase64, moduleName) DJBErpDecryt(encBase64, moduleName, + * groupSeq) + * + * 파라미터: encBase64 : Base64 인코딩된 암호문 (String) moduleName : DB에 등록된 암호화 모듈명 + * (String) groupSeq : 키 도출용 그룹 시퀀스 (String, 선택). 미전달 시 빈 context로 호출. + * + * 반환값: 복호화된 평문 (String) + */ +public class DJBErpDecryt extends PostfixMathCommand { + + public DJBErpDecryt() { + numberOfParameters = -1; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + if (curNumberOfParameters != 2) { + throw new ParseException("DJBErpDecryt: 파라미터는 2개개여야 합니다 (encBase64, moduleName"); + } + + String moduleName = inStack.pop().toString(); + String encBase64 = inStack.pop().toString(); + + try { + byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim()); + byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, new HashMap<>(), null, cipherBytes); + inStack.push(new String(plainBytes, StandardCharsets.UTF_8)); + } catch (ParseException e) { + throw e; + } catch (Exception e) { + throw new ParseException("DJBErpDecryt 복호화 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpEncryt.java b/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpEncryt.java new file mode 100644 index 0000000..dc2a510 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/transformer/function/DJBErpEncryt.java @@ -0,0 +1,52 @@ +package com.eactive.eai.custom.transformer.function; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Stack; + +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +import com.eactive.eai.common.security.CryptoModuleService; + +/** + * JEP 트랜스포머 함수 — DJB ERP 연동 암호화. + * + * 사용법: DJBErpEncryt(plainText, moduleName) DJBErpEncryt(plainText, moduleName, + * groupSeq) + * + * 파라미터: plainText : 암호화할 평문 (String) moduleName : DB에 등록된 암호화 모듈명 (String) + * groupSeq : 키 도출용 그룹 시퀀스 (String, 선택). 미전달 시 빈 context로 호출. + * + * 반환값: Base64 인코딩된 암호문 (String) + */ +public class DJBErpEncryt extends PostfixMathCommand { + + public DJBErpEncryt() { + numberOfParameters = -1; + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + if (curNumberOfParameters != 2) { + throw new ParseException("DJBErpEncryt: 파라미터는 2개여야 합니다 (plainText, moduleName)"); + } + + String moduleName = inStack.pop().toString(); + String plainText = inStack.pop().toString(); + + try { + byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, new HashMap<>(), null, + plainText.getBytes(StandardCharsets.UTF_8)); + inStack.push(Base64.getEncoder().encodeToString(cipherBytes)); + } catch (ParseException e) { + throw e; + } catch (Exception e) { + throw new ParseException("DJBErpEncryt 암호화 실패: " + e.getMessage()); + } + } +} diff --git a/src/test/java/com/eactive/eai/custom/transformer/function/DJBErpCryptoFunctionTest.java b/src/test/java/com/eactive/eai/custom/transformer/function/DJBErpCryptoFunctionTest.java new file mode 100644 index 0000000..867e59c --- /dev/null +++ b/src/test/java/com/eactive/eai/custom/transformer/function/DJBErpCryptoFunctionTest.java @@ -0,0 +1,220 @@ +package com.eactive.eai.custom.transformer.function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Stack; + +import org.junit.jupiter.api.AfterAll; +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.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.context.support.GenericApplicationContext; + +import com.eactive.eai.common.security.CryptoModuleService; +import com.eactive.eai.common.util.ApplicationContextProvider; + +@TestMethodOrder(MethodOrderer.DisplayName.class) +class DJBErpCryptoFunctionTest { + + private static final String MODULE_NAME = "TEST_MODULE"; + private static final String PLAIN_TEXT = "plaintext-value"; + private static final byte[] PLAIN_BYTES = PLAIN_TEXT.getBytes(StandardCharsets.UTF_8); + private static final byte[] CIPHER_BYTES = new byte[]{0x10, 0x20, 0x30, 0x40}; + private static final String ENC_BASE64 = Base64.getEncoder().encodeToString(CIPHER_BYTES); + + private static GenericApplicationContext ctx; + private static CryptoModuleService mockService; + + private DJBErpDecryt decryt; + private DJBErpEncryt encryt; + + // ========================================================================= + // 클래스 초기화 + // ========================================================================= + + @BeforeAll + static void setUpClass() throws Exception { + mockService = mock(CryptoModuleService.class); + + ctx = new GenericApplicationContext(); + ctx.getBeanFactory().registerSingleton("cryptoModuleService", mockService); + ctx.registerBeanDefinition("applicationContextProvider", + BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class) + .getBeanDefinition()); + ctx.refresh(); + } + + @AfterAll + static void tearDownClass() { + if (ctx != null) ctx.close(); + } + + @BeforeEach + void setUp() throws Exception { + reset(mockService); + when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(PLAIN_BYTES); + when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class))).thenReturn(CIPHER_BYTES); + decryt = new DJBErpDecryt(); + encryt = new DJBErpEncryt(); + } + + // ========================================================================= + // 1. DJBErpDecryt + // ========================================================================= + + @Test + @DisplayName("1-1. 정상 복호화: (encBase64, moduleName) → 평문이 스택에 push된다") + void decryt_normalCase_pushesPlainText() throws Exception { + Stack stack = buildStack(ENC_BASE64, MODULE_NAME); + setParamCount(decryt, 2); + + decryt.run(stack); + + assertEquals(PLAIN_TEXT, stack.pop()); + assertTrue(stack.isEmpty(), "결과 push 후 스택에 잔여 항목이 없어야 한다"); + verify(mockService).decrypt(eq(MODULE_NAME), anyMap(), isNull(), any(byte[].class)); + } + + @Test + @DisplayName("1-2. 파라미터 수가 2가 아니면 ParseException") + void decryt_wrongParamCount_throwsParseException() throws Exception { + Stack stack = buildStack(ENC_BASE64, MODULE_NAME); + setParamCount(decryt, 3); + + assertThrows(ParseException.class, () -> decryt.run(stack)); + } + + @Test + @DisplayName("1-3. 유효하지 않은 Base64 입력 → ParseException") + void decryt_invalidBase64_throwsParseException() throws Exception { + Stack stack = buildStack("not-valid-base64!!!", MODULE_NAME); + setParamCount(decryt, 2); + + assertThrows(ParseException.class, () -> decryt.run(stack)); + } + + @Test + @DisplayName("1-4. 암호화 서비스 예외 → 'DJBErpDecryt 복호화 실패' ParseException으로 래핑") + void decryt_serviceThrows_wrapsInParseException() throws Exception { + when(mockService.decrypt(any(), anyMap(), isNull(), any(byte[].class))) + .thenThrow(new RuntimeException("모듈 오류")); + Stack stack = buildStack(ENC_BASE64, MODULE_NAME); + setParamCount(decryt, 2); + + ParseException ex = assertThrows(ParseException.class, () -> decryt.run(stack)); + assertTrue(ex.getMessage().contains("DJBErpDecryt 복호화 실패")); + } + + @Test + @DisplayName("1-5. moduleName이 CryptoModuleService에 그대로 전달된다") + void decryt_moduleNamePassedToService() throws Exception { + String customModule = "CUSTOM_AES_MODULE"; + Stack stack = buildStack(ENC_BASE64, customModule); + setParamCount(decryt, 2); + + decryt.run(stack); + + verify(mockService).decrypt(eq(customModule), anyMap(), isNull(), any(byte[].class)); + } + + // ========================================================================= + // 2. DJBErpEncryt + // ========================================================================= + + @Test + @DisplayName("2-1. 정상 암호화: (plainText, moduleName) → Base64가 스택에 push된다") + void encryt_normalCase_pushesBase64() throws Exception { + Stack stack = buildStack(PLAIN_TEXT, MODULE_NAME); + setParamCount(encryt, 2); + + encryt.run(stack); + + assertEquals(ENC_BASE64, stack.pop()); + assertTrue(stack.isEmpty(), "결과 push 후 스택에 잔여 항목이 없어야 한다"); + verify(mockService).encrypt(eq(MODULE_NAME), anyMap(), isNull(), any(byte[].class)); + } + + @Test + @DisplayName("2-2. 파라미터 수가 2가 아니면 ParseException") + void encryt_wrongParamCount_throwsParseException() throws Exception { + Stack stack = buildStack(PLAIN_TEXT, MODULE_NAME); + setParamCount(encryt, 3); + + assertThrows(ParseException.class, () -> encryt.run(stack)); + } + + @Test + @DisplayName("2-3. 암호화 서비스 예외 → 'DJBErpEncryt 암호화 실패' ParseException으로 래핑") + void encryt_serviceThrows_wrapsInParseException() throws Exception { + when(mockService.encrypt(any(), anyMap(), isNull(), any(byte[].class))) + .thenThrow(new RuntimeException("모듈 오류")); + Stack stack = buildStack(PLAIN_TEXT, MODULE_NAME); + setParamCount(encryt, 2); + + ParseException ex = assertThrows(ParseException.class, () -> encryt.run(stack)); + assertTrue(ex.getMessage().contains("DJBErpEncryt 암호화 실패")); + } + + @Test + @DisplayName("2-4. moduleName이 CryptoModuleService에 그대로 전달된다") + void encryt_moduleNamePassedToService() throws Exception { + String customModule = "CUSTOM_AES_MODULE"; + Stack stack = buildStack(PLAIN_TEXT, customModule); + setParamCount(encryt, 2); + + encryt.run(stack); + + verify(mockService).encrypt(eq(customModule), anyMap(), isNull(), any(byte[].class)); + } + + @Test + @DisplayName("2-5. plainText가 UTF-8 바이트로 변환되어 서비스에 전달된다") + void encryt_plainTextEncodedAsUtf8() throws Exception { + String korean = "한글평문"; + Stack stack = buildStack(korean, MODULE_NAME); + setParamCount(encryt, 2); + + encryt.run(stack); + + verify(mockService).encrypt(eq(MODULE_NAME), anyMap(), isNull(), + eq(korean.getBytes(StandardCharsets.UTF_8))); + } + + // ========================================================================= + // 헬퍼 + // ========================================================================= + + @SuppressWarnings({"rawtypes", "unchecked"}) + private Stack buildStack(Object... params) { + Stack stack = new Stack(); + for (Object p : params) { + stack.push(p); + } + return stack; + } + + private void setParamCount(PostfixMathCommand cmd, int count) throws Exception { + Field field = PostfixMathCommand.class.getDeclaredField("curNumberOfParameters"); + field.setAccessible(true); + field.set(cmd, count); + } +}