Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
Expand All @@ -34,6 +39,7 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -45,6 +51,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@

@Data
public class AliasCreateRequest {

private String secret;

private String secret;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.verygood.security.coding.aliases;

import lombok.Data;

@Data
public class AliasCreateResponse {
private String alias;

public AliasCreateResponse(String alias) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be covered by @RequiredArgsConstructor which is a part of @Data annotation.
Reason why It's not there is the absence of @NotNull annotation.

this.alias = alias;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<AliasCreateResponse> 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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@

public interface AliasesService {

String redact(String data);

String reveal(String redacted);
}
Original file line number Diff line number Diff line change
@@ -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<Alias> encrypted = db.getAliasByKey(uuid);
return encrypted.map(alias -> CipherUtils.decrypt(alias.getData(), secretKey)).orElse(null);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/verygood/security/coding/dao/AliasDao.java
Original file line number Diff line number Diff line change
@@ -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<Alias> getAliasByKey(String key);
}
Original file line number Diff line number Diff line change
@@ -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<Alias, String> {

default boolean saveAlias(Alias alias) {
save(alias);
return true;
}

default Optional<Alias> getAliasByKey(String key) {
return findById(key);
}
}

Original file line number Diff line number Diff line change
@@ -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<String, String> db = new ConcurrentHashMap<>();

@Override
public boolean saveAlias(Alias alias) {
db.put(alias.getAlias(), alias.getData());
return false;
}

@Override
public Optional<Alias> 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();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.verygood.security.coding.exception;

public class CipherDecryptException extends RuntimeException {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need 2 identical exceptions?
Maybe we can pass MODE in message.

public CipherDecryptException(String message) {
super(message);
}

public CipherDecryptException(String message, Throwable cause) {
super(message, cause);
}

public CipherDecryptException(Throwable cause) {
super(cause);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/verygood/security/coding/model/Alias.java
Original file line number Diff line number Diff line change
@@ -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;
}
70 changes: 70 additions & 0 deletions src/main/java/com/verygood/security/coding/util/CipherUtils.java
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encrypt and decrypt have a lot of duplication.
Would be nice to have part of It extracted in separate method.

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);
}
}
}
3 changes: 2 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:aliases
7 changes: 7 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DROP TABLE IF EXISTS aliases;

CREATE TABLE aliases
(
alias VARCHAR(30) PRIMARY KEY,
data VARCHAR(250) NOT NULL
);
39 changes: 39 additions & 0 deletions src/test/java/com/verygood/security/coding/AliasesServiceTest.java
Original file line number Diff line number Diff line change
@@ -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() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need @RunWith(SpringRunner.class) here.
We don't need Spring context to test AliasesService logic.

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
class CodingApplicationTests {

Expand Down
Loading