diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..71e6ee0
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,44 @@
+name: Build and Release JAR
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+env:
+ KEYCLOAK_VERSION: 25.0.6
+
+jobs:
+ build-and-release:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+
+ - name: Install Git
+ run: sudo apt-get update && sudo apt-get install -y git
+
+ - name: Build with Maven
+ run: mvn clean package -Dkeycloak.version=$KEYCLOAK_VERSION -Drevision=${{ github.ref_name }}
+ env:
+ KEYCLOAK_VERSION: ${{ env.KEYCLOAK_VERSION }}
+
+ - name: Upload JAR artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: pii-encryption-provider
+ path: target/*.jar
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ files: target/*.jar
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Dockerfile b/Dockerfile
index c81e3d7..e0abaeb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,9 +6,9 @@ FROM maven:3-openjdk-17-slim AS provider-pii
ARG KEYCLOAK_VERSION
WORKDIR /app
COPY pom.xml .
-RUN mvn verify --fail-never -Dkeycloak.version=$KEYCLOAK_VERSION
+RUN mvn verify -B -Dkeycloak.version=$KEYCLOAK_VERSION
COPY src ./src
-RUN mvn package -o -Dkeycloak.version=$KEYCLOAK_VERSION
+RUN mvn test package -B -Dkeycloak.version=$KEYCLOAK_VERSION
### Build customized Keycloak
diff --git a/pom.xml b/pom.xml
index 070334f..0e595ff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,7 @@
17
25.0.1
JDK_17
+ 5.9.2
@@ -20,5 +21,42 @@
${keycloak.version}
provided
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+ test
+
+
+ org.apache.maven.surefire
+ surefire-junit-platform
+ 3.5.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.jupiter.version}
+ test
+
+
+ uk.org.webcompere
+ system-stubs-jupiter
+ 1.2.0
+ test
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.2
+
+
+ --add-opens java.base/java.util=ALL-UNNAMED
+
+
+
+
diff --git a/src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtils.java b/src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtils.java
index b788aef..e12764b 100644
--- a/src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtils.java
+++ b/src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtils.java
@@ -1,5 +1,6 @@
package my.unifi.eset.keycloak.piidataencryption.utils;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -107,19 +108,41 @@ public static boolean isEncryptedValue(String value) {
}
static synchronized SecretKey getEncryptionKey() throws NoSuchAlgorithmException {
- if (key == null) {
- String rawkey = System.getenv("KC_PII_ENCKEY");
- if (rawkey == null || rawkey.isBlank()) {
- MessageDigest md = MessageDigest.getInstance("MD5");
- md.update(System.getenv("KC_DB_URL").getBytes());
- rawkey = HexFormat.of().formatHex(md.digest()).toLowerCase();
- Logger.getLogger(EncryptionUtils.class).warnf("Encryption key generated using MD5 hash of KC_DB_URL envvar is %s. It is recommended to set this key as KC_PII_ENCKEY envvar.", rawkey);
- }
- key = new SecretKeySpec(rawkey.getBytes(), "AES");
+ if(key != null){
+ return key;
+ }
+
+ String rawkey = System.getenv("KC_PII_ENCKEY");
+ if (rawkey == null || rawkey.isBlank()) {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(System.getenv("KC_DB_URL").getBytes());
+ rawkey = HexFormat.of().formatHex(md.digest()).toLowerCase();
+ Logger.getLogger(EncryptionUtils.class).warn("Encryption key generated using MD5 hash of KC_DB_URL. It is recommended to set this key as KC_PII_ENCKEY envvar.");
}
+
+ SecretKeySpec genKey = new SecretKeySpec(rawkey.getBytes(), "AES");
+ try {
+ validateKey(genKey);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ }
+
+ key = genKey;
+
return key;
}
+ public static void validateKey(SecretKeySpec candidateKey) {
+ try {
+ Cipher cipher = Cipher.getInstance(algorithm);
+ cipher.init(Cipher.ENCRYPT_MODE, candidateKey, new IvParameterSpec(new byte[16]));
+ // Trivial encryption to validate
+ cipher.doFinal("test".getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid encryption key for algorithm " + algorithm, e);
+ }
+ }
+
private EncryptionUtils() {
}
diff --git a/src/test/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtilsTest.java b/src/test/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtilsTest.java
new file mode 100644
index 0000000..52ad1a6
--- /dev/null
+++ b/src/test/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtilsTest.java
@@ -0,0 +1,89 @@
+package my.unifi.eset.keycloak.piidataencryption.utils;
+
+import org.junit.jupiter.api.BeforeEach;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import uk.org.webcompere.systemstubs.environment.EnvironmentVariables;
+import uk.org.webcompere.systemstubs.jupiter.SystemStub;
+import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension;
+
+import java.security.NoSuchAlgorithmException;
+
+import static my.unifi.eset.keycloak.piidataencryption.utils.EncryptionUtils.algorithm;
+import static org.junit.jupiter.api.Assertions.*;
+
+@ExtendWith(SystemStubsExtension.class)
+class EncryptionUtilsTest {
+
+ @SystemStub
+ private EnvironmentVariables environmentVariables;
+
+ protected final String envVarKey = "KC_PII_ENCKEY";
+ protected final String validEncKey = "1234567891123456";
+
+ @BeforeEach
+ void setUp() {
+ EncryptionUtils.key = null;
+
+ environmentVariables.set(envVarKey, validEncKey);
+ }
+
+ @Test
+ void testEncryptValue() throws NoSuchAlgorithmException {
+ String encryptedValue = EncryptionUtils.encryptValue("test");
+ assertNotEquals("test", encryptedValue);
+
+ String decryptedValue = EncryptionUtils.decryptValue(encryptedValue);
+ assertEquals("test", decryptedValue);
+ }
+
+ @Test
+ void testDecryptValue() throws NoSuchAlgorithmException {
+ SecretKey key = EncryptionUtils.getEncryptionKey();
+
+ String decryptedValue = EncryptionUtils.decryptValue("$$$GTaogsGC8vbgE098AN9kC+UCHD8vYzVgFF0hFDnuKIw=");
+
+ assertEquals("test", decryptedValue);
+ }
+
+ @Test
+ void testWrongKeySize() {
+ environmentVariables.set(envVarKey, "invalid");
+
+ Throwable thrown = assertThrows(RuntimeException.class, () -> {
+ EncryptionUtils.getEncryptionKey();
+ });
+
+ assertEquals("Invalid encryption key for algorithm " + algorithm, thrown.getMessage());
+
+
+ environmentVariables.set(envVarKey, validEncKey);
+ assertDoesNotThrow(() -> EncryptionUtils.getEncryptionKey(), "should work once the key is correct and not reuse the previous one");
+ }
+
+ @Test
+ void testValidKey() {
+ byte[] validKeyBytes = validEncKey.getBytes();
+ SecretKeySpec validKey = new SecretKeySpec(validKeyBytes, "AES");
+
+ assertDoesNotThrow(() -> EncryptionUtils.validateKey(validKey));
+ }
+
+ @Test
+ void testValidateAnInvalidKey() {
+ byte[] invalidKeyBytes = "invalid_size".getBytes();
+ SecretKeySpec invalidKey = new SecretKeySpec(invalidKeyBytes, "AES");
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ EncryptionUtils.validateKey(invalidKey);
+ });
+
+ String expectedMessage = "Invalid encryption key for algorithm " + algorithm;
+ assertThrows(IllegalArgumentException.class, () -> EncryptionUtils.validateKey(invalidKey));
+ assert(thrown.getMessage().contains(expectedMessage));
+ }
+}