package com.mastercard.developer.interceptors; import com.google.api.client.http.*; import com.mastercard.developer.encryption.EncryptionException; import com.mastercard.developer.encryption.FieldLevelEncryptionConfig; import com.mastercard.developer.test.TestUtils; import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.junit.MockitoJUnitRunner; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import static com.mastercard.developer.test.TestUtils.assertPayloadEquals; import static com.mastercard.developer.test.TestUtils.getTestFieldLevelEncryptionConfigBuilder; import static org.hamcrest.core.Is.isA; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class HttpExecuteFieldLevelEncryptionInterceptorTest { private static final String JSON_TYPE = "application/json; charset=utf-8"; @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void testIntercept_ShouldEncryptRequestPayloadAndUpdateContentLengthHeader() throws Exception { // GIVEN FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withEncryptionPath("$.foo", "$.encryptedFoo") .build(); HttpRequest request = mock(HttpRequest.class); HttpHeaders httpHeaders = new HttpHeaders(); when(request.getContent()).thenReturn(new ByteArrayContent(JSON_TYPE, "{\"foo\":\"bar\"}".getBytes())); when(request.getHeaders()).thenReturn(httpHeaders); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.intercept(request); // THEN ArgumentCaptor<HttpContent> contentCaptor = ArgumentCaptor.forClass(HttpContent.class); verify(request).setContent(contentCaptor.capture()); ByteArrayOutputStream encryptedPayloadStream = new ByteArrayOutputStream(); contentCaptor.getValue().writeTo(encryptedPayloadStream); String encryptedPayload = encryptedPayloadStream.toString(StandardCharsets.UTF_8.name()); Assert.assertFalse(encryptedPayload.contains("foo")); Assert.assertTrue(encryptedPayload.contains("encryptedFoo")); assertEquals(encryptedPayload.length(), httpHeaders.getContentLength().intValue()); } @Test public void testIntercept_ShouldDoNothing_WhenNoPayload() throws Exception { // GIVEN FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withEncryptionPath("$.foo", "$.encryptedFoo") .build(); HttpRequest request = mock(HttpRequest.class); when(request.getContent()).thenReturn(null); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.intercept(request); // THEN verify(request).getContent(); verifyNoMoreInteractions(request); } @Test public void testIntercept_ShouldThrowIOException_WhenEncryptionFails() throws Exception { // GIVEN FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withEncryptionPath("$.foo", "$.encryptedFoo") .withEncryptionCertificate(TestUtils.getTestInvalidEncryptionCertificate()) // Invalid certificate .build(); HttpRequest request = mock(HttpRequest.class); when(request.getContent()).thenReturn(new ByteArrayContent(JSON_TYPE, "{\"foo\":\"bar\"}".getBytes())); // THEN expectedException.expect(IOException.class); expectedException.expectMessage("Failed to intercept and encrypt request!"); expectedException.expectCause(isA(EncryptionException.class)); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.intercept(request); } @Test public void testInterceptResponse_ShouldDecryptResponsePayloadAndUpdateContentLengthHeader() throws Exception { // GIVEN String encryptedPayload = "{" + " \"encryptedData\": {" + " \"iv\": \"a32059c51607d0d02e823faecda5fb15\"," + " \"encryptedKey\": \"a31cfe7a7981b72428c013270619554c1d645c04b9d51c7eaf996f55749ef62fd7c7f8d334f95913be41ae38c46d192670fd1acb84ebb85a00cd997f1a9a3f782229c7bf5f0fdf49fe404452d7ed4fd41fbb95b787d25893fbf3d2c75673cecc8799bbe3dd7eb4fe6d3f744b377572cdf8aba1617194e10475b6cd6a8dd4fb8264f8f51534d8f7ac7c10b4ce9c44d15066724b03a0ab0edd512f9e6521fdb5841cd6964e457d6b4a0e45ba4aac4e77d6bbe383d6147e751fa88bc26278bb9690f9ee84b17123b887be2dcef0873f4f9f2c895d90e23456fafb01b99885e31f01a3188f0ad47edf22999cc1d0ddaf49e1407375117b5d66f1f185f2b57078d255\"," + " \"encryptedValue\": \"21d754bdb4567d35d58720c9f8364075\"," + " \"oaepHashingAlgorithm\": \"SHA256\"" + " }" + "}"; FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withDecryptionPath("$.encryptedData", "$.data") .build(); HttpResponse response = mock(HttpResponse.class); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(100l); when(response.parseAsString()).thenReturn(encryptedPayload); when(response.getHeaders()).thenReturn(httpHeaders); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.interceptResponse(response); // THEN Field contentField = response.getClass().getDeclaredField("content"); contentField.setAccessible(true); InputStream payloadInputStream = (InputStream) contentField.get(response); String payload = IOUtils.toString(payloadInputStream, StandardCharsets.UTF_8); assertPayloadEquals("{\"data\":\"string\"}", payload); assertEquals(payload.length(), httpHeaders.getContentLength().intValue()); } @Test public void testInterceptResponse_ShouldDoNothing_WhenNoPayload() throws Exception { // GIVEN FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder().build(); HttpResponse response = mock(HttpResponse.class); when(response.parseAsString()).thenReturn(null); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.interceptResponse(response); // THEN verify(response).parseAsString(); verifyNoMoreInteractions(response); } @Test public void testInterceptResponse_ShouldThrowIOException_WhenDecryptionFails() throws Exception { // GIVEN String encryptedPayload = "{" + " \"encryptedData\": {" + " \"iv\": \"a2c494ca28dec4f3d6ce7d68b1044cfe\"," + " \"encryptedKey\": \"NOT A VALID KEY!\"," + " \"encryptedValue\": \"0672589113046bf692265b6ea6088184\"" + " }" + "}"; FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withDecryptionPath("$.encryptedData", "$.data") .build(); HttpResponse response = mock(HttpResponse.class); when(response.parseAsString()).thenReturn(encryptedPayload); // THEN expectedException.expect(IOException.class); expectedException.expectMessage("Failed to intercept and decrypt response!"); expectedException.expectCause(isA(EncryptionException.class)); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.interceptResponse(response); } @Test public void testIntercept_ShouldEncryptRequestPayloadAndAddEncryptionHttpHeaders_WhenRequestedInConfig() throws Exception { // GIVEN FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withEncryptionPath("$.foo", "$.encryptedFoo") .withIvHeaderName("x-iv") .withEncryptedKeyHeaderName("x-encrypted-key") .withOaepPaddingDigestAlgorithmHeaderName("x-oaep-padding-digest-algorithm") .withEncryptionCertificateFingerprintHeaderName("x-encryption-certificate-fingerprint") .withEncryptionKeyFingerprintHeaderName("x-encryption-key-fingerprint") .build(); HttpRequest request = mock(HttpRequest.class); HttpHeaders httpHeaders = new HttpHeaders(); when(request.getContent()).thenReturn(new ByteArrayContent(JSON_TYPE, "{\"foo\":\"bar\"}".getBytes())); when(request.getHeaders()).thenReturn(httpHeaders); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.intercept(request); // THEN ArgumentCaptor<HttpContent> contentCaptor = ArgumentCaptor.forClass(HttpContent.class); verify(request).setContent(contentCaptor.capture()); ByteArrayOutputStream encryptedPayloadStream = new ByteArrayOutputStream(); contentCaptor.getValue().writeTo(encryptedPayloadStream); String encryptedPayload = encryptedPayloadStream.toString(StandardCharsets.UTF_8.name()); Assert.assertFalse(encryptedPayload.contains("foo")); Assert.assertTrue(encryptedPayload.contains("encryptedFoo")); assertEquals(encryptedPayload.length(), httpHeaders.getContentLength().intValue()); assertNotNull(httpHeaders.get("x-iv")); assertNotNull(httpHeaders.get("x-encrypted-key")); assertEquals("SHA256", httpHeaders.get("x-oaep-padding-digest-algorithm")); assertEquals("80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279", httpHeaders.get("x-encryption-certificate-fingerprint")); assertEquals("761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79", httpHeaders.get("x-encryption-key-fingerprint")); } @Test public void testInterceptResponse_ShouldDecryptResponsePayloadAndRemoveEncryptionHttpHeaders_WhenRequestedInConfig() throws Exception { // GIVEN String encryptedPayload = "{" + " \"encryptedData\": {" + " \"encryptedValue\": \"21d754bdb4567d35d58720c9f8364075\"" + " }" + "}"; FieldLevelEncryptionConfig config = getTestFieldLevelEncryptionConfigBuilder() .withDecryptionPath("$.encryptedData", "$.data") .withIvHeaderName("x-iv") .withEncryptedKeyHeaderName("x-encrypted-key") .withOaepPaddingDigestAlgorithmHeaderName("x-oaep-padding-digest-algorithm") .withEncryptionCertificateFingerprintHeaderName("x-encryption-certificate-fingerprint") .withEncryptionKeyFingerprintHeaderName("x-encryption-key-fingerprint") .build(); HttpResponse response = mock(HttpResponse.class); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.set("x-iv", "a32059c51607d0d02e823faecda5fb15"); httpHeaders.set("x-encrypted-key", "a31cfe7a7981b72428c013270619554c1d645c04b9d51c7eaf996f55749ef62fd7c7f8d334f95913be41ae38c46d192670fd1acb84ebb85a00cd997f1a9a3f782229c7bf5f0fdf49fe404452d7ed4fd41fbb95b787d25893fbf3d2c75673cecc8799bbe3dd7eb4fe6d3f744b377572cdf8aba1617194e10475b6cd6a8dd4fb8264f8f51534d8f7ac7c10b4ce9c44d15066724b03a0ab0edd512f9e6521fdb5841cd6964e457d6b4a0e45ba4aac4e77d6bbe383d6147e751fa88bc26278bb9690f9ee84b17123b887be2dcef0873f4f9f2c895d90e23456fafb01b99885e31f01a3188f0ad47edf22999cc1d0ddaf49e1407375117b5d66f1f185f2b57078d255"); httpHeaders.set("x-oaep-padding-digest-algorithm", "SHA256"); httpHeaders.set("x-encryption-key-fingerprint", "761b003c1eade3a5490e5000d37887baa5e6ec0e226c07706e599451fc032a79"); httpHeaders.set("x-encryption-certificate-fingerprint", "80810fc13a8319fcf0e2ec322c82a4c304b782cc3ce671176343cfe8160c2279"); httpHeaders.setContentLength(100l); when(response.parseAsString()).thenReturn(encryptedPayload); when(response.getHeaders()).thenReturn(httpHeaders); // WHEN HttpExecuteFieldLevelEncryptionInterceptor instanceUnderTest = new HttpExecuteFieldLevelEncryptionInterceptor(config); instanceUnderTest.interceptResponse(response); // THEN Field contentField = response.getClass().getDeclaredField("content"); contentField.setAccessible(true); InputStream payloadInputStream = (InputStream) contentField.get(response); String payload = IOUtils.toString(payloadInputStream, StandardCharsets.UTF_8); assertPayloadEquals("{\"data\":\"string\"}", payload); assertEquals(payload.length(), httpHeaders.getContentLength().intValue()); assertNull(response.getHeaders().get("x-iv")); assertNull(response.getHeaders().get("x-encrypted-key")); assertNull(response.getHeaders().get("x-oaep-padding-digest-algorithm")); assertNull(response.getHeaders().get("x-encryption-key-fingerprint")); assertNull(response.getHeaders().get("x-encryption-certificate-fingerprint")); } }