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);
+ }
+ }
+
+}