Skip to content
This repository has been archived by the owner on Aug 12, 2023. It is now read-only.

Email encryption #24

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
34 changes: 34 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Docker
on:
push:
schedule:
- cron: '54 2 2 * *'
workflow_dispatch:
jobs:
buildDockerImage:
name: Build Docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/wisvch/feedback-tool
tags: type=sha, prefix={{date 'YYYYMMDD'}}-
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: ${{ github.ref == 'refs/heads/master' }}
29 changes: 0 additions & 29 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM wisvch/openjdk:11-jdk AS builder
FROM openjdk:11-jdk-buster AS builder
COPY . /src
WORKDIR /src
RUN ./gradlew build
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ dependencies {
compile 'org.projectlombok:lombok'
compile 'org.json:json:20180813'

compile 'org.apache.commons:commons-lang3:3.7'
runtime 'org.postgresql:postgresql'
runtime 'com.h2database:h2'

Expand Down
202 changes: 202 additions & 0 deletions src/main/java/ch/wisv/converters/AbstractCryptoConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package ch.wisv.converters;

import static ch.wisv.converters.KeyProperty.DATABASE_ENCRYPTION_KEY;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.persistence.AttributeConverter;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;

/**
* AbstractCryptoConverter class.
*
* @param <T>
*/
abstract class AbstractCryptoConverter<T> implements AttributeConverter<T, String> {

/**
* Used for concatenating cipher text and iv together.
* This will not cause issues when the text contains hashtags,
* since a Base64 encoded string cannot contain hashtags.
* https://en.wikipedia.org/wiki/Base64#Base64_table
*/
private static final String CONCATENATION = "####";

/** CipherInitializer. */
private CipherInitializer cipherInitializer;

/**
* AbstractCryptoConverter constructor.
*/
public AbstractCryptoConverter() {
this(new CipherInitializer());
}

/**
* AbstractCryptoConverter constructor.
*
* @param cipherInitializer of type CipherInitializer
*/
public AbstractCryptoConverter(CipherInitializer cipherInitializer) {
this.cipherInitializer = cipherInitializer;
}

/**
* Convert th entity attribute to the database data.
*
* @param attribute of type T
*
* @return String
*/
@Override
public String convertToDatabaseColumn(T attribute) {
if (isNotEmpty(DATABASE_ENCRYPTION_KEY) && isNotNullOrEmpty(attribute)) {
try {
Cipher cipher = cipherInitializer.prepareAndInitCipher(Cipher.ENCRYPT_MODE, DATABASE_ENCRYPTION_KEY, null);

return this.concatenatedCipherTextAndIv(this.encrypt(cipher, attribute), cipher.getIV());
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException |
NoSuchPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}

return this.entityAttributeToString(attribute);
}

/**
* Convert the database data to the entity attribute.
*
* @param dbData of type String.
*
* @return T
*/
@Override
public T convertToEntityAttribute(String dbData) {
if (isNotEmpty(DATABASE_ENCRYPTION_KEY) && isNotEmpty(dbData)) {
try {
Pair<String, byte[]> cipherTextAndIv = this.splitDbData(dbData);
Cipher cipher = cipherInitializer.prepareAndInitCipher(Cipher.DECRYPT_MODE, DATABASE_ENCRYPTION_KEY, cipherTextAndIv.getRight());

return this.decrypt(cipher, cipherTextAndIv.getLeft());
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException |
NoSuchPaddingException | IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}

return this.stringToEntityAttribute(dbData);
}

/**
* Concatenated cipher text and IV together.
*
* @param cipherText of type String
* @param iv of type byte[]
*
* @return String
*/
private String concatenatedCipherTextAndIv(String cipherText, byte[] iv) {
return cipherText + CONCATENATION + Base64.getEncoder().encodeToString(iv);
}

/**
* Split db data into cipher text and IV.
*
* @param dbData of type String
*
* @return Pair
*/
private Pair<String, byte[]> splitDbData(String dbData) {
String[] splitDbData = dbData.split(CONCATENATION);
String cipherText = splitDbData[0];
byte[] iv = Base64.getDecoder().decode(splitDbData[1]);

return new ImmutablePair<>(cipherText, iv);
}

/**
* Do final Cipher call.
*
* @param cipher of type String
* @param bytes of type byte[]
*
* @return byte[]
*
* @throws IllegalBlockSizeException when the block size is wrong
* @throws BadPaddingException when the padding is wrong
*/
private byte[] callCipherDoFinal(Cipher cipher, byte[] bytes) throws IllegalBlockSizeException, BadPaddingException {
return cipher.doFinal(bytes);
}

/**
* Decrypt database data with a give cipher.
*
* @param cipher of type Cipher
* @param dbData of type String
*
* @return T
*
* @throws IllegalBlockSizeException when the block size is wrong
* @throws BadPaddingException when the padding is wrong
*/
private T decrypt(Cipher cipher, String dbData) throws IllegalBlockSizeException, BadPaddingException {
byte[] encryptedBytes = Base64.getDecoder().decode(dbData);
byte[] decryptedBytes = this.callCipherDoFinal(cipher, encryptedBytes);

return this.stringToEntityAttribute(new String(decryptedBytes));
}

/**
* Encrypt attribute with a give cipher.
*
* @param cipher of type Cipher
* @param attribute of type T
*
* @return String
*
* @throws IllegalBlockSizeException when the block size is wrong
* @throws BadPaddingException when the padding is wrong
*/
private String encrypt(Cipher cipher, T attribute) throws IllegalBlockSizeException, BadPaddingException {
byte[] bytesToEncrypt = this.entityAttributeToString(attribute).getBytes();
byte[] encryptedBytes = this.callCipherDoFinal(cipher, bytesToEncrypt);

return Base64.getEncoder().encodeToString(encryptedBytes);
}

/**
* Check attribute is not null or empty.
*
* @param attribute of type T
*
* @return boolean
*/
abstract boolean isNotNullOrEmpty(T attribute);

/**
* Convert database data to entity attribute.
*
* @param dbData of type String
*
* @return T
*/
abstract T stringToEntityAttribute(String dbData);

/**
* Convert entity attribute to database data.
*
* @param attribute of type T
*
* @return String
*/
abstract String entityAttributeToString(T attribute);
}
67 changes: 67 additions & 0 deletions src/main/java/ch/wisv/converters/CipherInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ch.wisv.converters;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
* CipherInitializer.
*/
class CipherInitializer {

/** Encryption method. */
private static final String CIPHER_INSTANCE_NAME = "AES/CBC/PKCS5Padding";

/** Secret key algorithm. */
private static final String SECRET_KEY_ALGORITHM = "AES";

/**
* Creates an IvParameterSpec object using the bytes in iv as the IV.
* The IV is fixed such that we do not have to save the IV.
* TODO: Random IV to improve security.
*
* @param cipher of type Cipher
*
* @return AlgorithmParameterSpec
*/
private AlgorithmParameterSpec getRandomIv(Cipher cipher) {
SecureRandom randomSecureRandom = new SecureRandom();
byte[] iv = new byte[cipher.getBlockSize()];
randomSecureRandom.nextBytes(iv);

return new IvParameterSpec(iv);
}

/**
* Prepare Cipher by adding the key and creating an IV.
*
* @param encryptionMode of type int
* @param key of type String
* @param iv of type byte[]
*
* @return Cipher
*
* @throws InvalidKeyException when key is invalid
* @throws NoSuchPaddingException when padding method does not exists
* @throws NoSuchAlgorithmException when the algorithm does not exists
* @throws InvalidAlgorithmParameterException when the mode of operation does not exists
*/
Cipher prepareAndInitCipher(int encryptionMode, String key, byte[] iv)
throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE_NAME);
Key secretKey = new SecretKeySpec(key.getBytes(), SECRET_KEY_ALGORITHM);

AlgorithmParameterSpec algorithmParameters = iv == null ? this.getRandomIv(cipher) : new IvParameterSpec(iv);
cipher.init(encryptionMode, secretKey, algorithmParameters);

return cipher;
}
}
23 changes: 23 additions & 0 deletions src/main/java/ch/wisv/converters/KeyProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ch.wisv.converters;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* KeyProperty component.
*/
@Component
public class KeyProperty {

/** Encryption key. */
static String DATABASE_ENCRYPTION_KEY;

/**
* Set encryption key.
*/
@Value("${wisvch.database.encryption.key}")
public void setDatabase(String databaseEncryptionKey) {
DATABASE_ENCRYPTION_KEY = databaseEncryptionKey;
}

}
Loading