feat: CryptoFilter GCM AAD 지원 추가 및 단위테스트 작성
- CryptoFilter: crypto.aad.header 프로퍼티 + buildAad() 메서드 추가 - CryptoFilter: 기본 scope SCOPE_BODY → SCOPE_FIELD 변경 - CryptoFilter: decryptBody/decryptField/encryptBody/encryptField aad 파라미터 적용 - BaseCryptoFilterTest: BODY/FIELD scope, buildAad, AAD 헤더 연동 단위테스트 신규 작성 - CryptoModuleServiceTest: STATIC/DYNAMIC GCM AAD 테스트 섹션 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,19 +35,24 @@ import com.eactive.eai.util.JsonPathUtil;
|
||||
* crypto.dec.to.path 복호화 결과 반영 경로. "/" = 전체 body 교체.
|
||||
* crypto.enc.from.path 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
|
||||
* crypto.enc.to.path 암호화 결과(Base64)를 넣을 JSON 경로 (예: /encryptedData)
|
||||
*
|
||||
* GCM AAD 전용 (선택):
|
||||
* crypto.aad.header AAD로 사용할 HTTP 요청 헤더명 (예: X-Api-Request-Id).
|
||||
* 미설정 또는 헤더값 없으면 aad=null → IV를 AAD 대체값으로 사용.
|
||||
*/
|
||||
public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROP_MODULE_NAME = "crypto.module.name";
|
||||
public static final String PROP_SCOPE = "crypto.scope";
|
||||
public static final String PROP_DEC_ENABLED = "crypto.dec.enabled";
|
||||
public static final String PROP_ENC_ENABLED = "crypto.enc.enabled";
|
||||
public static final String PROP_MODULE_NAME = "crypto.module.name";
|
||||
public static final String PROP_SCOPE = "crypto.scope";
|
||||
public static final String PROP_DEC_ENABLED = "crypto.dec.enabled";
|
||||
public static final String PROP_ENC_ENABLED = "crypto.enc.enabled";
|
||||
public static final String PROP_DEC_FROM_PATH = "crypto.dec.from.path";
|
||||
public static final String PROP_DEC_TO_PATH = "crypto.dec.to.path";
|
||||
public static final String PROP_ENC_FROM_PATH = "crypto.enc.from.path";
|
||||
public static final String PROP_ENC_TO_PATH = "crypto.enc.to.path";
|
||||
public static final String PROP_AAD_HEADER = "crypto.aad.header";
|
||||
|
||||
protected static final String SCOPE_BODY = "BODY";
|
||||
protected static final String SCOPE_FIELD = "FIELD";
|
||||
@@ -79,13 +84,14 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
String scope = prop.getProperty(PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return decryptField(body, moduleName, runtimeCtx,
|
||||
return decryptField(body, moduleName, runtimeCtx, aad,
|
||||
required(prop, PROP_DEC_FROM_PATH),
|
||||
prop.getProperty(PROP_DEC_TO_PATH, PATH_ROOT));
|
||||
}
|
||||
return decryptBody(body, moduleName, runtimeCtx);
|
||||
return decryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -104,22 +110,23 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
String scope = prop.getProperty(PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(resultMessage);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return encryptField(body, moduleName, runtimeCtx,
|
||||
return encryptField(body, moduleName, runtimeCtx, aad,
|
||||
prop.getProperty(PROP_ENC_FROM_PATH, PATH_ROOT),
|
||||
required(prop, PROP_ENC_TO_PATH));
|
||||
}
|
||||
return encryptBody(body, moduleName, runtimeCtx);
|
||||
return encryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 복호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx) throws Exception {
|
||||
private String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes);
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
|
||||
return new String(plainBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@@ -127,7 +134,7 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
* fromPath의 Base64 값을 복호화하여 toPath에 반영.
|
||||
* toPath = "/" → 복호화된 텍스트로 body 전체 교체.
|
||||
*/
|
||||
private String decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
private String decryptField(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad,
|
||||
String fromPath, String toPath) throws Exception {
|
||||
|
||||
String encBase64 = JsonPathUtil.getValueAtPath(body, fromPath);
|
||||
@@ -137,7 +144,7 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
}
|
||||
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim());
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, cipherBytes);
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
|
||||
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (PATH_ROOT.equals(toPath)) {
|
||||
@@ -150,8 +157,8 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
// 암호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
private String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx) throws Exception {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx,
|
||||
private String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(cipherBytes);
|
||||
}
|
||||
@@ -160,7 +167,7 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
* fromPath 값을 암호화하여 toPath(Base64)에 반영.
|
||||
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑.
|
||||
*/
|
||||
private String encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
private String encryptField(String body, String moduleName, Map<String, String> runtimeCtx, byte[] aad,
|
||||
String fromPath, String toPath) throws Exception {
|
||||
|
||||
String plainText;
|
||||
@@ -174,7 +181,7 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
}
|
||||
}
|
||||
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx,
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
plainText.getBytes(StandardCharsets.UTF_8));
|
||||
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
|
||||
|
||||
@@ -208,6 +215,19 @@ public abstract class CryptoFilter implements HttpAdapterFilter {
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* crypto.aad.header 프로퍼티에 지정된 헤더값을 UTF-8 바이트로 반환한다.
|
||||
* 프로퍼티 미설정 또는 헤더값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
|
||||
*/
|
||||
protected byte[] buildAad(Properties prop, HttpServletRequest request) {
|
||||
String headerName = prop.getProperty(PROP_AAD_HEADER);
|
||||
if (StringUtils.isBlank(headerName) || request == null) {
|
||||
return null;
|
||||
}
|
||||
String headerValue = request.getHeader(headerName);
|
||||
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private boolean isEnabled(Properties prop, String key, String defaultVal) {
|
||||
return !"N".equalsIgnoreCase(prop.getProperty(key, defaultVal));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* BaseCryptoFilter / CryptoFilter 단위 테스트.
|
||||
* CryptoModuleService는 Mock으로 대체하여 필터 로직(Base64, JSON 경로, 프로퍼티 파싱)만 검증한다.
|
||||
*/
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class BaseCryptoFilterTest {
|
||||
|
||||
private static final String MODULE = "TEST_MODULE";
|
||||
private static final String PLAIN_STR = "decrypted-plain-text";
|
||||
private static final byte[] PLAIN = PLAIN_STR.getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CIPHER = new byte[]{0x10, 0x20, 0x30, 0x40, 0x50, 0x60};
|
||||
private static final String CIPHER_B64 = Base64.getEncoder().encodeToString(CIPHER);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleService mockService;
|
||||
private static BaseCryptoFilter filter;
|
||||
|
||||
private HttpServletRequest mockRequest;
|
||||
private HttpServletResponse mockResponse;
|
||||
|
||||
@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();
|
||||
|
||||
filter = new BaseCryptoFilter();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
reset(mockService);
|
||||
mockRequest = mock(HttpServletRequest.class);
|
||||
mockResponse = mock(HttpServletResponse.class);
|
||||
|
||||
when(mockService.decrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(PLAIN);
|
||||
when(mockService.encrypt(anyString(), anyMap(), any(), any(byte[].class))).thenReturn(CIPHER);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. buildRuntimeContext
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. buildRuntimeContext — 항상 빈 Map 반환")
|
||||
void testBuildRuntimeContext_returnsEmptyMap() {
|
||||
Map<String, String> result = filter.buildRuntimeContext(new Properties(), mockRequest);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. buildAad
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. buildAad — crypto.aad.header 미설정 → null")
|
||||
void testBuildAad_noProp_returnsNull() {
|
||||
assertNull(filter.buildAad(new Properties(), mockRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. buildAad — 헤더명 설정, 헤더값 없음 → null")
|
||||
void testBuildAad_headerNameSet_noValue_returnsNull() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(CryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(null);
|
||||
|
||||
assertNull(filter.buildAad(prop, mockRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-3. buildAad — 헤더명 설정, 헤더값 있음 → UTF-8 바이트 반환")
|
||||
void testBuildAad_headerNameSet_valuePresent_returnsByte() {
|
||||
String headerVal = "req-id-abc123";
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(CryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(headerVal);
|
||||
|
||||
assertArrayEquals(headerVal.getBytes(StandardCharsets.UTF_8), filter.buildAad(prop, mockRequest));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. doPreFilter — BODY scope 복호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. BODY scope — Base64 암호문 복호화 → 평문 반환")
|
||||
void testPreFilter_body_decrypt_roundTrip() throws Exception {
|
||||
Object result = filter.doPreFilter("g", "a", CIPHER_B64, bodyDecryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. crypto.dec.enabled=N — 복호화 생략, message 그대로 반환")
|
||||
void testPreFilter_decDisabled_passThrough() throws Exception {
|
||||
Properties prop = bodyDecryptProps();
|
||||
prop.setProperty(CryptoFilter.PROP_DEC_ENABLED, "N");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(CIPHER_B64, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. crypto.module.name 누락 — IllegalArgumentException")
|
||||
void testPreFilter_missingModuleName_throwsException() {
|
||||
Properties prop = new Properties();
|
||||
prop.setProperty(CryptoFilter.PROP_SCOPE, "BODY");
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. message가 byte[] — UTF-8 디코딩 후 처리")
|
||||
void testPreFilter_body_byteArrayMessage() throws Exception {
|
||||
Object result = filter.doPreFilter("g", "a", CIPHER_B64.getBytes(StandardCharsets.UTF_8),
|
||||
bodyDecryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. doPreFilter — FIELD scope 복호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. FIELD scope — fromPath 복호화 → toPath 반영")
|
||||
void testPreFilter_field_decryptToPath() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertTrue(result.toString().contains("\"result\":\"" + PLAIN_STR + "\""),
|
||||
"toPath에 복호화된 값이 반영되어야 한다");
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(), isNull(), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. FIELD scope, toPath='/' — 복호화된 텍스트로 body 전체 교체")
|
||||
void testPreFilter_field_decryptToRoot() throws Exception {
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(PLAIN_STR, result, "toPath='/'이면 body 전체가 복호화된 텍스트로 교체되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPreFilter_field_missingFromPathValue_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/other");
|
||||
|
||||
Object result = filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. doPostFilter — BODY scope 암호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. BODY scope, crypto.enc.enabled=Y — 평문 암호화 → Base64 반환")
|
||||
void testPostFilter_body_encrypt() throws Exception {
|
||||
String body = "plain-response-body";
|
||||
Object result = filter.doPostFilter("g", "a", body, bodyEncryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(CIPHER_B64, result);
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), eq(body.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. crypto.enc.enabled=N(기본) — 암호화 생략, message 그대로 반환")
|
||||
void testPostFilter_encDisabled_passThrough() throws Exception {
|
||||
String body = "plain-response-body";
|
||||
Object result = filter.doPostFilter("g", "a", body, bodyDecryptProps(), mockRequest, mockResponse);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 6. doPostFilter — FIELD scope 암호화
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("6-1. FIELD scope — fromPath 평문 암호화 → toPath Base64 반영")
|
||||
void testPostFilter_field_encryptToPath() throws Exception {
|
||||
String body = "{\"plain\":\"hello\",\"enc\":\"\"}";
|
||||
Properties prop = fieldEncryptProps("/plain", "/enc");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertTrue(result.toString().contains("\"enc\":\"" + CIPHER_B64 + "\""),
|
||||
"toPath에 Base64 암호문이 반영되어야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-2. FIELD scope, fromPath='/' — body 전체 암호화 → toPath 필드명으로 래핑")
|
||||
void testPostFilter_field_encryptFromRoot() throws Exception {
|
||||
String body = "{\"plain\":\"hello\"}";
|
||||
Properties prop = fieldEncryptProps("/", "/encrypted");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals("{\"encrypted\":\"" + CIPHER_B64 + "\"}", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("6-3. FIELD scope — fromPath 값 없음 → body 그대로 반환")
|
||||
void testPostFilter_field_missingFromPathValue_passThrough() throws Exception {
|
||||
String body = "{\"other\":\"value\"}";
|
||||
Properties prop = fieldEncryptProps("/plain", "/enc");
|
||||
|
||||
Object result = filter.doPostFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
assertEquals(body, result);
|
||||
verifyNoInteractions(mockService);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 7. AAD 헤더 연동
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("7-1. doPreFilter BODY — crypto.aad.header 설정 시 헤더값을 AAD로 전달")
|
||||
void testPreFilter_body_aadFromHeader_passedToService() throws Exception {
|
||||
String aadVal = "request-id-xyz";
|
||||
Properties prop = bodyDecryptProps();
|
||||
prop.setProperty(CryptoFilter.PROP_AAD_HEADER, "X-Api-Request-Id");
|
||||
when(mockRequest.getHeader("X-Api-Request-Id")).thenReturn(aadVal);
|
||||
|
||||
filter.doPreFilter("g", "a", CIPHER_B64, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(),
|
||||
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-2. doPostFilter BODY — crypto.aad.header 미설정 시 aad=null로 전달")
|
||||
void testPostFilter_body_noAadHeader_nullAadPassedToService() throws Exception {
|
||||
filter.doPostFilter("g", "a", "plain", bodyEncryptProps(), mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).encrypt(eq(MODULE), anyMap(), isNull(), any(byte[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("7-3. doPreFilter FIELD — crypto.aad.header 설정 시 헤더값을 AAD로 전달")
|
||||
void testPreFilter_field_aadFromHeader_passedToService() throws Exception {
|
||||
String aadVal = "field-aad-value";
|
||||
String body = "{\"enc\":\"" + CIPHER_B64 + "\",\"result\":\"\"}";
|
||||
Properties prop = fieldDecryptProps("/enc", "/result");
|
||||
prop.setProperty(CryptoFilter.PROP_AAD_HEADER, "X-Api-Aad");
|
||||
when(mockRequest.getHeader("X-Api-Aad")).thenReturn(aadVal);
|
||||
|
||||
filter.doPreFilter("g", "a", body, prop, mockRequest, mockResponse);
|
||||
|
||||
verify(mockService).decrypt(eq(MODULE), anyMap(),
|
||||
eq(aadVal.getBytes(StandardCharsets.UTF_8)), eq(CIPHER));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private Properties bodyDecryptProps() {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(CryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(CryptoFilter.PROP_SCOPE, "BODY");
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties bodyEncryptProps() {
|
||||
Properties p = bodyDecryptProps();
|
||||
p.setProperty(CryptoFilter.PROP_ENC_ENABLED, "Y");
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties fieldDecryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(CryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(CryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(CryptoFilter.PROP_DEC_FROM_PATH, fromPath);
|
||||
p.setProperty(CryptoFilter.PROP_DEC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
|
||||
private Properties fieldEncryptProps(String fromPath, String toPath) {
|
||||
Properties p = new Properties();
|
||||
p.setProperty(CryptoFilter.PROP_MODULE_NAME, MODULE);
|
||||
p.setProperty(CryptoFilter.PROP_SCOPE, "FIELD");
|
||||
p.setProperty(CryptoFilter.PROP_ENC_ENABLED, "Y");
|
||||
p.setProperty(CryptoFilter.PROP_ENC_FROM_PATH, fromPath);
|
||||
p.setProperty(CryptoFilter.PROP_ENC_TO_PATH, toPath);
|
||||
return p;
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,13 @@ class CryptoModuleServiceTest {
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
when(mockHsm.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
|
||||
CryptoModuleConfig aes = buildStatic("AES_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig aria = buildStatic("ARIA_SVC", "ARIA", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig ecb = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding");
|
||||
CryptoModuleConfig dyn = buildDynamic("DYN_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn));
|
||||
CryptoModuleConfig aes = buildStatic("AES_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig aria = buildStatic("ARIA_SVC", "ARIA", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig ecb = buildStaticNoIv("AES_ECB_SVC", "AES", "ECB", "NoPadding");
|
||||
CryptoModuleConfig dyn = buildDynamic("DYN_SVC", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig gcm = buildStatic("AES_GCM_SVC", "AES", "GCM", "NoPadding");
|
||||
CryptoModuleConfig gcmDyn = buildDynamic("DYN_GCM_SVC", "AES", "GCM", "NoPadding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(aes, aria, ecb, dyn, gcm, gcmDyn));
|
||||
|
||||
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
@@ -200,6 +202,124 @@ class CryptoModuleServiceTest {
|
||||
"NoPadding 모드에서 블록 크기 미정렬 평문은 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 4. STATIC AAD — encrypt(name, aad, plain) / decrypt(name, aad, cipher)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("4-1. STATIC AES/GCM — AAD 명시 encrypt → decrypt 라운드트립")
|
||||
void testStaticGcm_explicitAad_roundTrip() throws Exception {
|
||||
byte[] aad = "request-header-ctx".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "GCM AAD 서비스 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
|
||||
byte[] decrypted = service.decrypt("AES_GCM_SVC", aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-2. STATIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
|
||||
void testStaticGcm_nullAad_usesIvAsAad() throws Exception {
|
||||
byte[] plain = "AAD null 기본 동작 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", (byte[]) null, plain);
|
||||
byte[] decrypted = service.decrypt("AES_GCM_SVC", (byte[]) null, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-3. STATIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
|
||||
void testStaticGcm_wrongAad_throwsAuthException() throws Exception {
|
||||
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "GCM 인증 실패 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_GCM_SVC", aad, plain);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.decrypt("AES_GCM_SVC", wrongAad, encrypted),
|
||||
"잘못된 AAD로 GCM 복호화 시 인증 실패 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4-4. STATIC CBC — AAD 파라미터는 무시되고 정상 복호화")
|
||||
void testStaticCbc_aadIgnored_normalDecrypt() throws Exception {
|
||||
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("AES_SVC", aad, plain);
|
||||
byte[] decrypted = service.decrypt("AES_SVC", aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. DYNAMIC AAD — encrypt(name, ctx, aad, plain) / decrypt(name, ctx, aad, cipher)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. DYNAMIC AES/GCM — runtimeContext + AAD 명시 encrypt → decrypt 라운드트립")
|
||||
void testDynamicGcm_explicitAad_roundTrip() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "dynamic-gcm-aad-ctx".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "DYNAMIC GCM AAD 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. DYNAMIC AES/GCM — AAD null이면 IV를 AAD로 사용하는 기본 동작")
|
||||
void testDynamicGcm_nullAad_usesIvAsAad() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] plain = "DYNAMIC GCM AAD null 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, null, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_GCM_SVC", runtimeCtx, null, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. DYNAMIC AES/GCM — 다른 AAD로 복호화 시 인증 실패 예외")
|
||||
void testDynamicGcm_wrongAad_throwsAuthException() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "correct-aad-value".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] wrongAad = "wrong-aad-value!!".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "DYNAMIC GCM 인증 실패".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_GCM_SVC", runtimeCtx, aad, plain);
|
||||
|
||||
assertThrows(Exception.class,
|
||||
() -> service.decrypt("DYN_GCM_SVC", runtimeCtx, wrongAad, encrypted),
|
||||
"잘못된 AAD로 DYNAMIC GCM 복호화 시 인증 실패 예외가 발생해야 한다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-4. DYNAMIC CBC — AAD 파라미터는 무시되고 정상 복호화")
|
||||
void testDynamicCbc_aadIgnored_normalDecrypt() throws Exception {
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKey0000001");
|
||||
|
||||
byte[] aad = "ignored-aad".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] plain = "DYNAMIC CBC AAD 무시 테스트".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
byte[] encrypted = service.encrypt("DYN_SVC", runtimeCtx, aad, plain);
|
||||
byte[] decrypted = service.decrypt("DYN_SVC", runtimeCtx, aad, encrypted);
|
||||
|
||||
assertArrayEquals(plain, decrypted, "DYNAMIC CBC 모드에서 AAD는 무시되고 정상 암복호화되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user