Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encryption key validation #3

Merged
merged 7 commits into from
Feb 6, 2025
Merged
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
44 changes: 44 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was there a reason to have the offline parameter?

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, the reason being the mvn command is splitted into two to optimize Docker builds. The mvn verify after COPY pom.xml will first download all dependencies as defined by the pom.xml. The mvn package -o after the COPY src ./src will compile the source code using the dependencies already downloaded by mvn verify. Without the offline parameter, maven somehow still attempts to check and re-download dependencies even though the dependencies are all there.

With Docker build automatically re-uses the caches if no files have changed, we don't want to re-download dependencies when we just modify files inside the src folder.

So, yes the offline flag is very intentionally added to optimize Docker builds. Please revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a535479 i moved the tests to be ran together with verify and skipped them in the package step. This because tests required some more packages to be downloaded.
I also removed the --fail-never in verify because we don't want failed tests to be ignored, you agree? The question is why do we have that in verify.. ?

Copy link
Owner

Choose a reason for hiding this comment

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

The unit tests require the src folder, don't they? If so, then they cannot be run with the mvn verify command because at that point only pom.xml is copied over into the Docker build layer. Only after the COPY src ./src command that the src folder is available inside the layer for the unit tests to execute.

Whatever packages needed by the unit tests are declared inside pom.xml so I think mvn verify should already download the packages. If it doesn't then maybe we need to tweak the mvn verify parameters to also download <scope>test</scope>

Here was basically my original intention:

  1. Add pom.xml into Docker build layer
  2. Download dependencies declared in the pom.xml including any plugins needed for packaging
  3. Add the rest of the source code inside src folder
  4. Do the actual packaging into JAR

This way, if I don't change anything inside pom.xml then Docker build engine will not need to perform step 1 & 2 and thus saving a lot of time by re-using the Docker build caches, skipping the re-download of dependencies.

RUN mvn test package -B -Dkeycloak.version=$KEYCLOAK_VERSION

### Build customized Keycloak

Expand Down
38 changes: 38 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<maven.compiler.target>17</maven.compiler.target>
<keycloak.version>25.0.1</keycloak.version>
<netbeans.hint.jdkPlatform>JDK_17</netbeans.hint.jdkPlatform>
<junit.jupiter.version>5.9.2</junit.jupiter.version>
</properties>
<dependencies>
<dependency>
Expand All @@ -20,5 +21,42 @@
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-jupiter</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- Tells the JVM when running tests to open java.util for reflection by unnamed modules. Required by system-stubs-jupiter -->
<argLine>--add-opens java.base/java.util=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}