From 337c9b93e90d0354a4630f958b984b00f6dbb4ca Mon Sep 17 00:00:00 2001 From: Roman Savko Date: Thu, 23 Apr 2020 19:55:38 +0300 Subject: [PATCH] Basic implementation --- pom.xml | 11 +++ .../coding/aliases/AliasCreateRequest.java | 4 +- .../coding/aliases/AliasCreateResponse.java | 12 ++++ .../coding/aliases/AliasesController.java | 31 ++++---- .../coding/aliases/AliasesService.java | 3 + .../coding/aliases/AliasesServiceImpl.java | 42 +++++++++++ .../security/coding/dao/AliasDao.java | 12 ++++ .../security/coding/dao/AliasRepository.java | 21 ++++++ .../coding/dao/MockAliasRepository.java | 32 +++++++++ .../exception/CipherDecryptException.java | 15 ++++ .../exception/CipherEncryptException.java | 15 ++++ .../verygood/security/coding/model/Alias.java | 14 ++++ .../security/coding/util/CipherUtils.java | 70 +++++++++++++++++++ src/main/resources/application.properties | 3 +- src/main/resources/schema.sql | 7 ++ .../security/coding/AliasesServiceTest.java | 39 +++++++++++ .../coding/CodingApplicationTests.java | 1 + .../coding/TestAliasRESTController.java | 48 +++++++++++++ 18 files changed, 363 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/verygood/security/coding/aliases/AliasCreateResponse.java create mode 100644 src/main/java/com/verygood/security/coding/aliases/AliasesServiceImpl.java create mode 100644 src/main/java/com/verygood/security/coding/dao/AliasDao.java create mode 100644 src/main/java/com/verygood/security/coding/dao/AliasRepository.java create mode 100644 src/main/java/com/verygood/security/coding/dao/MockAliasRepository.java create mode 100644 src/main/java/com/verygood/security/coding/exception/CipherDecryptException.java create mode 100644 src/main/java/com/verygood/security/coding/exception/CipherEncryptException.java create mode 100644 src/main/java/com/verygood/security/coding/model/Alias.java create mode 100644 src/main/java/com/verygood/security/coding/util/CipherUtils.java create mode 100644 src/main/resources/schema.sql create mode 100644 src/test/java/com/verygood/security/coding/AliasesServiceTest.java create mode 100644 src/test/java/com/verygood/security/coding/TestAliasRESTController.java diff --git a/pom.xml b/pom.xml index d33d98e..202243e 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,11 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + com.h2database h2 @@ -34,6 +39,7 @@ lombok true + org.springframework.boot spring-boot-starter-test @@ -45,6 +51,11 @@ + + junit + junit + test + diff --git a/src/main/java/com/verygood/security/coding/aliases/AliasCreateRequest.java b/src/main/java/com/verygood/security/coding/aliases/AliasCreateRequest.java index 2df9e2d..3dfc4b4 100644 --- a/src/main/java/com/verygood/security/coding/aliases/AliasCreateRequest.java +++ b/src/main/java/com/verygood/security/coding/aliases/AliasCreateRequest.java @@ -4,7 +4,5 @@ @Data public class AliasCreateRequest { - - private String secret; - + private String secret; } diff --git a/src/main/java/com/verygood/security/coding/aliases/AliasCreateResponse.java b/src/main/java/com/verygood/security/coding/aliases/AliasCreateResponse.java new file mode 100644 index 0000000..7fbbda0 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/aliases/AliasCreateResponse.java @@ -0,0 +1,12 @@ +package com.verygood.security.coding.aliases; + +import lombok.Data; + +@Data +public class AliasCreateResponse { + private String alias; + + public AliasCreateResponse(String alias) { + this.alias = alias; + } +} diff --git a/src/main/java/com/verygood/security/coding/aliases/AliasesController.java b/src/main/java/com/verygood/security/coding/aliases/AliasesController.java index 83ed036..b4af3f4 100644 --- a/src/main/java/com/verygood/security/coding/aliases/AliasesController.java +++ b/src/main/java/com/verygood/security/coding/aliases/AliasesController.java @@ -1,25 +1,30 @@ package com.verygood.security.coding.aliases; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; @Slf4j @RestController @RequestMapping("/aliases") public class AliasesController { - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createAlias(@RequestBody final AliasCreateRequest createRequest){ - log.info("Creating alias for: " + createRequest.getSecret()); - return new ResponseEntity<>("Created", HttpStatus.CREATED); - } + private final AliasesService aliasesService; + + @Autowired + public AliasesController(AliasesService aliasesService) { + this.aliasesService = aliasesService; + } + + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createAlias(@RequestBody AliasCreateRequest createRequest) { + log.info("Creating alias for: " + createRequest.getSecret()); + String alias = aliasesService.redact(createRequest.getSecret()); + log.info("Alias created: {} => {}", createRequest.getSecret(), alias); + return new ResponseEntity(new AliasCreateResponse(alias), HttpStatus.CREATED); + } } diff --git a/src/main/java/com/verygood/security/coding/aliases/AliasesService.java b/src/main/java/com/verygood/security/coding/aliases/AliasesService.java index 53707bc..6397381 100644 --- a/src/main/java/com/verygood/security/coding/aliases/AliasesService.java +++ b/src/main/java/com/verygood/security/coding/aliases/AliasesService.java @@ -2,4 +2,7 @@ public interface AliasesService { + String redact(String data); + + String reveal(String redacted); } diff --git a/src/main/java/com/verygood/security/coding/aliases/AliasesServiceImpl.java b/src/main/java/com/verygood/security/coding/aliases/AliasesServiceImpl.java new file mode 100644 index 0000000..e10dad0 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/aliases/AliasesServiceImpl.java @@ -0,0 +1,42 @@ +package com.verygood.security.coding.aliases; + +import com.verygood.security.coding.dao.AliasDao; +import com.verygood.security.coding.model.Alias; +import com.verygood.security.coding.util.CipherUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.UUID; + +@Service +public class AliasesServiceImpl implements AliasesService { + final String secretKey = "Very__$ecure#@Key!@"; + + private final AliasDao db; + + @Autowired + public AliasesServiceImpl(AliasDao db) { + this.db = db; + } + + @Override + public String redact(String data) { + String encryptedString = CipherUtils.encrypt(data, secretKey); + String id = UUID.randomUUID().toString(); + + Alias alias = new Alias(); + alias.setAlias(id); + alias.setData(encryptedString); + db.saveAlias(alias); + + return id; + + } + + @Override + public String reveal(String uuid) { + Optional encrypted = db.getAliasByKey(uuid); + return encrypted.map(alias -> CipherUtils.decrypt(alias.getData(), secretKey)).orElse(null); + } +} diff --git a/src/main/java/com/verygood/security/coding/dao/AliasDao.java b/src/main/java/com/verygood/security/coding/dao/AliasDao.java new file mode 100644 index 0000000..09ca7f3 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/dao/AliasDao.java @@ -0,0 +1,12 @@ +package com.verygood.security.coding.dao; + +import com.verygood.security.coding.model.Alias; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface AliasDao { + boolean saveAlias(Alias alias); + Optional getAliasByKey(String key); +} diff --git a/src/main/java/com/verygood/security/coding/dao/AliasRepository.java b/src/main/java/com/verygood/security/coding/dao/AliasRepository.java new file mode 100644 index 0000000..d7ec6a0 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/dao/AliasRepository.java @@ -0,0 +1,21 @@ +package com.verygood.security.coding.dao; + +import com.verygood.security.coding.model.Alias; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository("db") +public interface AliasRepository extends AliasDao, CrudRepository { + + default boolean saveAlias(Alias alias) { + save(alias); + return true; + } + + default Optional getAliasByKey(String key) { + return findById(key); + } +} + diff --git a/src/main/java/com/verygood/security/coding/dao/MockAliasRepository.java b/src/main/java/com/verygood/security/coding/dao/MockAliasRepository.java new file mode 100644 index 0000000..90ab83e --- /dev/null +++ b/src/main/java/com/verygood/security/coding/dao/MockAliasRepository.java @@ -0,0 +1,32 @@ +package com.verygood.security.coding.dao; + +import com.verygood.security.coding.model.Alias; +import org.springframework.stereotype.Repository; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Repository("mock") +public class MockAliasRepository implements AliasDao { + private final Map db = new ConcurrentHashMap<>(); + + @Override + public boolean saveAlias(Alias alias) { + db.put(alias.getAlias(), alias.getData()); + return false; + } + + @Override + public Optional getAliasByKey(String key) { + if(db.containsKey(key)) { + Alias alias = new Alias(); + alias.setAlias(key); + alias.setData(db.get(key)); + return Optional.of(alias); + } + return Optional.empty(); + } + + +} diff --git a/src/main/java/com/verygood/security/coding/exception/CipherDecryptException.java b/src/main/java/com/verygood/security/coding/exception/CipherDecryptException.java new file mode 100644 index 0000000..2564324 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/exception/CipherDecryptException.java @@ -0,0 +1,15 @@ +package com.verygood.security.coding.exception; + +public class CipherDecryptException extends RuntimeException { + public CipherDecryptException(String message) { + super(message); + } + + public CipherDecryptException(String message, Throwable cause) { + super(message, cause); + } + + public CipherDecryptException(Throwable cause) { + super(cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/verygood/security/coding/exception/CipherEncryptException.java b/src/main/java/com/verygood/security/coding/exception/CipherEncryptException.java new file mode 100644 index 0000000..fae7a4b --- /dev/null +++ b/src/main/java/com/verygood/security/coding/exception/CipherEncryptException.java @@ -0,0 +1,15 @@ +package com.verygood.security.coding.exception; + +public class CipherEncryptException extends RuntimeException { + public CipherEncryptException(String message) { + super(message); + } + + public CipherEncryptException(String message, Throwable cause) { + super(message, cause); + } + + public CipherEncryptException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/verygood/security/coding/model/Alias.java b/src/main/java/com/verygood/security/coding/model/Alias.java new file mode 100644 index 0000000..17b3d72 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/model/Alias.java @@ -0,0 +1,14 @@ +package com.verygood.security.coding.model; + +import lombok.Data; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity(name = "aliases") +@Data +public class Alias { + @Id + private String alias; + private String data; +} diff --git a/src/main/java/com/verygood/security/coding/util/CipherUtils.java b/src/main/java/com/verygood/security/coding/util/CipherUtils.java new file mode 100644 index 0000000..0343b72 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/util/CipherUtils.java @@ -0,0 +1,70 @@ +package com.verygood.security.coding.util; + +import com.verygood.security.coding.exception.CipherDecryptException; +import com.verygood.security.coding.exception.CipherEncryptException; +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +@Slf4j +public enum CipherUtils { + ; + private static final String DEFAULT_ALGORITHM = "AES/ECB/PKCS5Padding"; + private static SecretKeySpec secretKey; + + public static String encrypt(String strToEncrypt, String secret) { + return encrypt(strToEncrypt, secret, DEFAULT_ALGORITHM); + } + + public static String encrypt(String whatToEncrypt, String secret, String algorithm) throws CipherEncryptException { + requireNonNull(whatToEncrypt); + requireNonNull(secret); + try { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(secret)); + return Base64.getEncoder().encodeToString(cipher.doFinal(whatToEncrypt.getBytes(UTF_8))); + } catch (Exception e) { + log.error("Error encrypting: " + e); + throw new CipherEncryptException(e); + } + } + + public static String decrypt(String strToEncrypt, String secret) { + return decrypt(strToEncrypt, secret, DEFAULT_ALGORITHM); + } + + public static String decrypt(String whatToDecrypt, String secret, String algorithm) throws CipherDecryptException { + requireNonNull(whatToDecrypt); + requireNonNull(secret); + try { + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(secret)); + return new String(cipher.doFinal(Base64.getDecoder().decode(whatToDecrypt))); + } catch (Exception e) { + log.error("Error decrypting: " + e); + throw new CipherDecryptException(e); + } + } + + + private static SecretKeySpec getSecretKey(String myKey) { + try { + byte[] key = myKey.getBytes(UTF_8); + MessageDigest sha = MessageDigest.getInstance("SHA-1"); + key = sha.digest(key); + key = Arrays.copyOf(key, 16); + return new SecretKeySpec(key, "AES"); + } catch (NoSuchAlgorithmException e) { + log.error("Exception setting key", e); + throw new IllegalStateException("Failed to create SecretKeySpec", e); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..e066c1e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ - +spring.h2.console.enabled=true +spring.datasource.url=jdbc:h2:mem:aliases \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..55abb15 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS aliases; + +CREATE TABLE aliases +( + alias VARCHAR(30) PRIMARY KEY, + data VARCHAR(250) NOT NULL +); \ No newline at end of file diff --git a/src/test/java/com/verygood/security/coding/AliasesServiceTest.java b/src/test/java/com/verygood/security/coding/AliasesServiceTest.java new file mode 100644 index 0000000..a67d6d9 --- /dev/null +++ b/src/test/java/com/verygood/security/coding/AliasesServiceTest.java @@ -0,0 +1,39 @@ +package com.verygood.security.coding; + +import com.verygood.security.coding.aliases.AliasesService; +import com.verygood.security.coding.aliases.AliasesServiceImpl; +import com.verygood.security.coding.dao.MockAliasRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.jupiter.api.Assertions.*; + +@RunWith(SpringRunner.class) +@Slf4j +public class AliasesServiceTest { + private AliasesService aliasesService = new AliasesServiceImpl(new MockAliasRepository()); + + @Test + public void inputIsEncryted() { + String input = "1111 2222 3333 4567"; + String redacted = aliasesService.redact(input); + log.info("{} => {}", input, redacted); + assertNotEquals(input, redacted); + } + + @Test(expected = NullPointerException.class) + public void inputIsValid() { + String redacted = aliasesService.redact(null); + fail(); + } + + @Test + public void canDecryptAlias() { + String input = "1111 2222 3333 4567"; + String redacted = aliasesService.redact(input); + String revealed = aliasesService.reveal(redacted); + assertEquals(input, revealed); + } +} diff --git a/src/test/java/com/verygood/security/coding/CodingApplicationTests.java b/src/test/java/com/verygood/security/coding/CodingApplicationTests.java index e3ac01a..f766fdc 100644 --- a/src/test/java/com/verygood/security/coding/CodingApplicationTests.java +++ b/src/test/java/com/verygood/security/coding/CodingApplicationTests.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; + @SpringBootTest class CodingApplicationTests { diff --git a/src/test/java/com/verygood/security/coding/TestAliasRESTController.java b/src/test/java/com/verygood/security/coding/TestAliasRESTController.java new file mode 100644 index 0000000..9e967f1 --- /dev/null +++ b/src/test/java/com/verygood/security/coding/TestAliasRESTController.java @@ -0,0 +1,48 @@ +package com.verygood.security.coding; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.verygood.security.coding.aliases.AliasCreateRequest; +import com.verygood.security.coding.aliases.AliasesController; +import com.verygood.security.coding.aliases.AliasesServiceImpl; +import com.verygood.security.coding.dao.MockAliasRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = {AliasesController.class, AliasesServiceImpl.class, MockAliasRepository.class}) +@AutoConfigureMockMvc +public class TestAliasRESTController { + @Autowired + private MockMvc mvc; + + @Test + public void aliasCreated() throws Exception { + AliasCreateRequest request = new AliasCreateRequest(); + request.setSecret("1111 2222 3333 4444"); + mvc.perform( + MockMvcRequestBuilders.post("/aliases") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) + .content(asJsonString(request)) + ) + .andExpect(status().isCreated()); + } + + private String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +}