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