Skip to content

Commit

Permalink
Merge pull request #2 from youabledev/v0.2.0
Browse files Browse the repository at this point in the history
V0.2.0
  • Loading branch information
youabledev authored Dec 19, 2024
2 parents 4ea8e87 + c0b1e99 commit d0836fd
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 4 deletions.
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Spring Safe HTTP

Spring Boot 웹 서버 애플리케이션의 HTTP 바디 데이터를 암호화하여 응답하고 암호화 된 데이터를 복호화해서 받을 수 있는 라이브러리 입니다.
Spring Boot 웹 서버 애플리케이션의 HTTP 바디 데이터를 암호화하여 응답하고 암호화 된 데이터를 복호화해서 받을 수 있는 라이브러리 입니다.


## How to
Expand All @@ -14,7 +13,7 @@ Spring Boot 웹 서버 애플리케이션의 HTTP 바디 데이터를 암호화
2. Add the dependency
```
dependencies {
implementation 'com.github.youabledev:spring-safe-http:Tag'
implementation 'com.github.youabledev:spring-safe-http:v0.2.0'
}
```
### maven
Expand All @@ -36,5 +35,30 @@ Spring Boot 웹 서버 애플리케이션의 HTTP 바디 데이터를 암호화
</dependency>
```

## Example
```java
@EncryptResponse
@DecryptRequest
@PostMapping("/regist")
public ResponseEntity<String> regist(
@RequestBody BookRequest request
) {
return ResponseEntity.ok().body(request.toString());
}
```
- request body를 복호화 하고자 할 때 @DecryptRequest를 사용합니다.
- response body를 암호화 하고자 할 때 @EncryptResponse를 사용합니다.
```
// request body
/PJ/p778x9sf8G0YXUUxGwn5NRhrPu4gWqwxKxDdQMxwq+wxJAxOucVvZSmzEnjNu/Z41THSQKzaQn8IVEZxfg==
```
- 암호화된 request body는 복호화되어 Controller의 parameter 객체로 바인딩 됩니다.
```
// response body
MTBIpe9JkN7EI9tFA6Fi8RuT1+mRUsHGn75VJLuJDfyQWib05UV7dHFOyiFUQrav
```
- Controller에서 return되는 ResponseEntity의 response body는 암호화 됩니다.


## LICENSE
spring-safe-http 라이브러리의 LICENSE는 [LICENSE](https://github.com/youabledev/spring-safe-http/blob/main/LICENSE)를 확인하세요
7 changes: 6 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ java {
}

group = 'com.youable'
version = '0.1.4'
version = '0.2.0'

repositories {
mavenCentral()
Expand All @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.aspectj:aspectjweaver:1.9.22.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
testImplementation 'org.mockito:mockito-core:4.8.0'
}

publishing {
Expand All @@ -42,4 +43,8 @@ publishing {
url = uri("https://jitpack.io")
}
}
}

tasks.test {
useJUnitPlatform() // Ensure JUnit 5 is used for tests
}
50 changes: 50 additions & 0 deletions src/main/java/com/youable/safehttp/cipher/AESCipher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.youable.safehttp.cipher;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AESCipher implements HttpCipher {
private final String privateKey;
private final String ALGORITHM = "AES";
private final int BEGIN_INDEX = 0;
private final int END_INDEX = 16;
private final String TRANSFORMATION = "AES/CBC/PKCS5Padding";

@Override
public String encrypt(String data) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(privateKey), ALGORITHM);
IvParameterSpec IV = new IvParameterSpec(privateKey.substring(BEGIN_INDEX, END_INDEX).getBytes());
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV);
byte[] encryptByte = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encryptByte);
}

@Override
public String decrypt(String data) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(Base64.getDecoder().decode(privateKey), ALGORITHM);
IvParameterSpec IV = new IvParameterSpec(privateKey.substring(BEGIN_INDEX, END_INDEX).getBytes());
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, IV);
byte[] decodeByte = cipher.doFinal(Base64.getDecoder().decode(data));
return new String(decodeByte);
}

private AESCipher(AESCipherBuilder builder) {
this.privateKey = builder.privateKey;
}

public static class AESCipherBuilder {
private String privateKey;

public AESCipherBuilder(String PRIVATE_KEY) {
this.privateKey = PRIVATE_KEY;
}

public AESCipher build() {
return new AESCipher(this);
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/youable/safehttp/cipher/HttpCipher.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
/**
* <p>use the httpCipher interface to implement the encryption/decryption algorithm.
* The object you implement must be registered as a bean.
* <pre>{@code
* @Configuration
* public class CipherConfig {
* @Bean
* @Primary
* public HttpCipher aesCipher() {
* return new AESCipher.AESCipherBuilder("3mlng8uGI+k2CGVDKR/EAwI+uN5pzoITSkREjzxZc7M=").build();
* }
* }</pre>
* <p>Create Configuration to enroll HttpCipher</p>
* @author youabledev
* @since 0.0.1
* @see com.youable.safehttp.advice.DecryptRequestBodyAdvice
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/com/youable/safehttp/cipher/RSACipher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.youable.safehttp.cipher;

import javax.crypto.Cipher;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

public class RSACipher implements HttpCipher {
private PublicKey publicKey;
private PrivateKey privateKey;

@Override
public String encrypt(String data) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}

@Override
public String decrypt(String data) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(data));
return new String(decryptedBytes);
}

private RSACipher(RSACipherBuilder builder) {
this.publicKey = builder.publicKey;
this.privateKey = builder.privateKey;
}

public static class RSACipherBuilder {
private PublicKey publicKey;
private PrivateKey privateKey;

public RSACipherBuilder(PublicKey publicKey, PrivateKey privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}

public RSACipher build() {
return new RSACipher(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.youable.safehttp.advice;

import com.youable.safehttp.annotation.DecryptRequest;
import com.youable.safehttp.cipher.HttpCipher;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;

import java.io.ByteArrayInputStream;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class DecryptRequestBodyAdviceTest {
@DisplayName("DecryptRequestBodyAdvice should support methods annotated with @DecryptRequest")
@Test
void testDecryptRequestAnnotation() {
// given
HttpCipher mockHttpCiper = mock(HttpCipher.class);
DecryptRequestBodyAdvice advice = new DecryptRequestBodyAdvice(mockHttpCiper);
MethodParameter methodParameter = mock(MethodParameter.class);

// when
when(methodParameter.hasMethodAnnotation(DecryptRequest.class)).thenReturn(true);
boolean supports = advice.supports(methodParameter, String.class, null);

// then
assert(supports);
}

@DisplayName("Decrypted request body should match expected JSON")
@Test
void testBeforeBodyRead() throws Exception {
// given
String encryptedBody = "SXuMtzsL8X05TCRuyOHEGA==";
String decryptedBody = "{\"key\":\"value\"}";
HttpCipher mockHttpCipher = mock(HttpCipher.class);
when(mockHttpCipher.decrypt(encryptedBody)).thenReturn(decryptedBody);

DecryptRequestBodyAdvice advice = new DecryptRequestBodyAdvice(mockHttpCipher);

HttpHeaders headers = new HttpHeaders();
DecryptedHttpInputMessage inputMessage = new DecryptedHttpInputMessage(encryptedBody, headers);

MethodParameter parameter = mock(MethodParameter.class);

// when
HttpInputMessage result = advice.beforeBodyRead(inputMessage, parameter, String.class, null);

// then
assertNotNull(result);
assertEquals(decryptedBody, new String(result.getBody().readAllBytes()));
}

@DisplayName("Decryption failure should throw RuntimeException")
@Test
void testDecryptFailure() throws Exception {
// given
String encryptedBody = "SXuMtzsL8X05TCRuyOHEGA==";
HttpCipher mockHttpCipher = mock(HttpCipher.class);
when(mockHttpCipher.decrypt(encryptedBody)).thenThrow(new RuntimeException("Decryption failed"));

DecryptRequestBodyAdvice advice = new DecryptRequestBodyAdvice(mockHttpCipher);

HttpHeaders headers = new HttpHeaders();
DecryptedHttpInputMessage inputMessage = new DecryptedHttpInputMessage(encryptedBody, headers);

MethodParameter parameter = mock(MethodParameter.class);

// when & then
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
advice.beforeBodyRead(inputMessage, parameter, String.class, null);
});

assertEquals("Decryption failed", exception.getCause().getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.youable.safehttp.aop;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.youable.safehttp.cipher.HttpCipher;
import org.aspectj.lang.ProceedingJoinPoint;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class EncryptResponseAspectTest {
@DisplayName("EncryptResponseAspect should match expected Response Body")
@Test
void testEncryptResponse() throws Throwable {
// given
ObjectMapper objectMapper = new ObjectMapper();
HttpCipher mockHttpCipher = mock(HttpCipher.class);
EncryptResponseAspect aspect = new EncryptResponseAspect(mockHttpCipher, objectMapper);

ProceedingJoinPoint mockJoinPoint = mock(ProceedingJoinPoint.class);

Person response = new Person("hongkildong", 12);
String responseJson = objectMapper.writeValueAsString(response);
String encryptedData = "o3Y8Yiuffa+LNkTftpgSGmBasze4poDDbuQ3GVIwmiY=";
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
ResponseEntity<Person> mockResponseEntity =
new ResponseEntity<>(response, headers, HttpStatus.OK);

// when
when(mockJoinPoint.proceed()).thenReturn(mockResponseEntity);
when(mockHttpCipher.encrypt(responseJson)).thenReturn(encryptedData);

// Act
Object result = aspect.encryptResponse(mockJoinPoint, null);

// then
assertTrue(result instanceof ResponseEntity<?>);
ResponseEntity<?> responseEntity = (ResponseEntity<?>) result;
assertEquals(encryptedData, responseEntity.getBody());
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
assertEquals(headers, responseEntity.getHeaders());

verify(mockJoinPoint, times(1)).proceed();
verify(mockHttpCipher, times(1)).encrypt(responseJson);
}

private record Person(String name, Integer age) {}
}
34 changes: 34 additions & 0 deletions src/test/java/com/youable/safehttp/cipher/AESCipherTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.youable.safehttp.cipher;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.security.SecureRandom;
import java.util.Base64;

import static org.junit.jupiter.api.Assertions.*;

public class AESCipherTest {
@DisplayName("AES encrypt/decrypt")
@Test
void testEncryptAndDecrypt() throws Exception {
// given
byte[] key = new byte[32];
new SecureRandom().nextBytes(key);
String privateKey = Base64.getEncoder().encodeToString(key);

HttpCipher cipher = new AESCipher.AESCipherBuilder(privateKey).build();

String plainText = "{\"key\":\"value\"}";

// when
String encryptedText = cipher.encrypt(plainText);
String decryptedText = cipher.decrypt(encryptedText);

// then
assertNotNull(encryptedText);
assertNotEquals(plainText, encryptedText);
assertEquals(plainText, decryptedText);
}

}
33 changes: 33 additions & 0 deletions src/test/java/com/youable/safehttp/cipher/RSACipherTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.youable.safehttp.cipher;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;

import static org.junit.jupiter.api.Assertions.*;

class RSACipherTest {
@DisplayName("RSA encrypt/decrypt")
@Test
void test() throws Exception {
// given
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048, new SecureRandom());
KeyPair keyPair = keyPairGenerator.generateKeyPair();

HttpCipher cipher = new RSACipher.RSACipherBuilder(keyPair.getPublic(), keyPair.getPrivate()).build();
String plainText = "{\"key\":\"value\"}";

// when
String encryptedText = cipher.encrypt(plainText);
String decryptedText = cipher.decrypt(encryptedText);

// then
assertNotNull(encryptedText);
assertNotEquals(plainText, encryptedText);
assertEquals(plainText, decryptedText);
}
}

0 comments on commit d0836fd

Please sign in to comment.