Skip to content

Commit aaf6358

Browse files
committed
Resolved GH-11: unable to generate encryption key when DB URL is defined using configuration parameter instead of environment variable
1 parent 2fca62e commit aaf6358

File tree

8 files changed

+59
-61
lines changed

8 files changed

+59
-61
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG KEYCLOAK_VERSION=26.0.6
1+
ARG KEYCLOAK_VERSION=26.3.0
22

33
### Build provider keycloak-pii-data-encryption
44

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ cd Keycloak-PII-Data-Encryption-Provider
2020
Compile this provider into a JAR file using the following command. JDK 17 or above and Maven are required to be pre-installed on the machine. Make sure to match the `keycloak.version` parameter with the version of the target Keycloak.
2121

2222
```shell
23-
mvn clean package -Dkeycloak.version=26.1.1
23+
mvn clean package -Dkeycloak.version=26.3.0
2424
```
2525

2626
Copy paste the packaged JAR file from inside `target` folder into Keycloak `providers` folder. Run `kc.sh build` command to get Keycloak to register this provider.
@@ -31,7 +31,7 @@ Use this method if this provider needs to be pre-packaged inside a custom Keyclo
3131

3232
```dockerfile
3333
# ARG defined before FROM in multi-staged Dockerfile is shared among the stages
34-
ARG KEYCLOAK_VERSION=26.1.1
34+
ARG KEYCLOAK_VERSION=26.3.0
3535

3636
# Build the provider
3737
FROM maven:3.8.1-openjdk-17-slim AS keycloak-pii-data-encryption
@@ -58,11 +58,11 @@ RUN /opt/keycloak/bin/kc.sh build --db=mysql --features="declarative-ui" --spi-u
5858

5959
### Setting the encryption key
6060

61-
This provider requires the encryption key to be provided via environment variable **`KC_PII_ENCKEY`** and it needs to be **at least 16 characters long**. There is, however, a default fallback that uses MD5 hash of environment variable `KC_DB_URL` if the encryption key is not provided. If you rely on this fallback and in the future need to migrate your Keycloak data into another databases that results in a different value of `KC_DB_URL`, you need to get the old value of `KC_DB_URL`, encode it using lowercased MD5 hash and set the value to the `KC_PII_ENCKEY` environment variable.
61+
This provider requires the encryption key to be provided via environment variable **`KC_PII_ENCKEY`** and it needs to be **at least 16 characters long**. If the encryption key is not provided, however, there is a default fallback that uses MD5 hash of the database JDBC URL, either using configuration parameter `db-url` or environment variable `KC_DB_URL`. If you rely on this fallback and in the future need to migrate your Keycloak data into another databases that results in a different value of JDBC URL, you need to get the old value of JDBC URL, encode it using lowercased MD5 hash and set the value to the `KC_PII_ENCKEY` environment variable.
6262

6363
### Enabling 'jpa-encrypted' user provider and 'declarative-ui' feature
6464

65-
Starting with version 2.0, this provider requires the Keycloak instance to be either built or started with two flags:
65+
This provider requires the Keycloak instance to be either built or started with two flags:
6666

6767
- `--spi-user-provider=jpa-encrypted`
6868
- `--features="declarative-ui"`

pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>my.unifi.eset</groupId>
55
<artifactId>keycloak-pii-data-encryption</artifactId>
6-
<version>2.3</version>
6+
<version>2.4</version>
77
<packaging>jar</packaging>
88
<properties>
99
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1010
<maven.compiler.release>17</maven.compiler.release>
1111
<maven.compiler.source>17</maven.compiler.source>
1212
<maven.compiler.target>17</maven.compiler.target>
13-
<keycloak.version>24.0.0</keycloak.version>
13+
<keycloak.version>26.3.0</keycloak.version>
1414
<netbeans.hint.jdkPlatform>JDK_17</netbeans.hint.jdkPlatform>
1515
<junit.jupiter.version>5.9.2</junit.jupiter.version>
1616
<surefire.version>3.5.2</surefire.version>
@@ -22,6 +22,12 @@
2222
<version>${keycloak.version}</version>
2323
<scope>provided</scope>
2424
</dependency>
25+
<dependency>
26+
<groupId>org.keycloak</groupId>
27+
<artifactId>keycloak-quarkus-server</artifactId>
28+
<version>${keycloak.version}</version>
29+
<scope>provided</scope>
30+
</dependency>
2531
<dependency>
2632
<groupId>org.apache.maven.plugins</groupId>
2733
<artifactId>maven-surefire-plugin</artifactId>

src/main/java/my/unifi/eset/keycloak/piidataencryption/jpa/EncryptedUserAttributeEntity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class EncryptedUserAttributeEntity {
4545
@Column(name = "NAME", length = 255)
4646
protected String name;
4747

48-
@Column(name = "VALUE", length = 1000)
48+
@Column(name = "VALUE")
4949
protected String value;
5050

5151
public EncryptedUserAttributeEntity() {

src/main/java/my/unifi/eset/keycloak/piidataencryption/listeners/EventListener.java

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package my.unifi.eset.keycloak.piidataencryption.listeners;
1818

1919
import com.fasterxml.jackson.core.JsonProcessingException;
20-
import com.fasterxml.jackson.databind.JsonNode;
2120
import com.fasterxml.jackson.databind.ObjectMapper;
2221
import my.unifi.eset.keycloak.piidataencryption.utils.LogicUtils;
2322
import jakarta.persistence.EntityManager;
@@ -27,7 +26,6 @@
2726
import org.keycloak.events.EventListenerProvider;
2827
import org.keycloak.events.EventType;
2928
import org.keycloak.events.admin.AdminEvent;
30-
import org.keycloak.events.admin.OperationType;
3129
import org.keycloak.events.admin.ResourceType;
3230
import org.keycloak.models.KeycloakSession;
3331
import org.keycloak.models.UserModel;
@@ -59,10 +57,6 @@ public EventListener(KeycloakSession session) {
5957
public void onEvent(Event event) {
6058
if (event.getType() == EventType.REGISTER || event.getType() == EventType.UPDATE_PROFILE) {
6159
UserModel user = session.users().getUserById(session.realms().getRealm(event.getRealmId()), event.getUserId());
62-
if (!LogicUtils.isUserEncryptionEnabled(session, event.getRealmId())) {
63-
logger.debugf("Event: USER_ENCRYPTION_SKIPPED, Realm: %s, User: %s", event.getRealmId(), user.getId());
64-
return;
65-
}
6660
encryptUserWithId(event.getRealmId(), user.getId());
6761
}
6862
}
@@ -75,27 +69,26 @@ public void onEvent(Event event) {
7569
*/
7670
@Override
7771
public void onEvent(AdminEvent event, boolean bln) {
78-
if (event.getResourceType() == ResourceType.USER) {
79-
String userId = null;
80-
if (event.getOperationType() == OperationType.CREATE) {
72+
if (event.getResourceType() != ResourceType.USER) {
73+
return;
74+
}
75+
String userId = switch (event.getOperationType()) {
76+
case CREATE -> {
8177
try {
82-
JsonNode json = (new ObjectMapper()).readTree(event.getRepresentation());
83-
String username = json.get("username").asText();
78+
String username = (new ObjectMapper()).readTree(event.getRepresentation()).get("username").asText();
8479
UserModel user = session.users().getUserByUsername(session.realms().getRealm(event.getRealmId()), username);
85-
userId = user != null ? user.getId() : null;
80+
yield (user != null ? user.getId() : null);
8681
} catch (JsonProcessingException ex) {
82+
yield null;
8783
}
8884
}
89-
if (event.getOperationType() == OperationType.UPDATE) {
90-
userId = event.getResourcePath().split("/")[1];
91-
}
92-
if (userId != null) {
93-
if (!LogicUtils.isUserEncryptionEnabled(session, event.getRealmId())) {
94-
logger.debugf("Event: USER_ENCRYPTION_SKIPPED, Realm: %s, User: %s", event.getRealmId(), userId);
95-
return;
96-
}
97-
encryptUserWithId(event.getRealmId(), userId);
98-
}
85+
case UPDATE ->
86+
event.getResourcePath().split("/")[1];
87+
default ->
88+
null;
89+
};
90+
if (userId != null) {
91+
encryptUserWithId(event.getRealmId(), userId);
9992
}
10093
}
10194

@@ -104,14 +97,18 @@ public void close() {
10497
}
10598

10699
private void encryptUserWithId(String realmId, String userId) {
107-
logger.debugf("Event: USER_ENCRYPTION, Realm: %s, User: %s", realmId, userId);
108-
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
109-
UserEntity userEntity = LogicUtils.getUserEntity(em, userId);
110-
LogicUtils.encryptUserEntity(session, em, userEntity);
111-
for (UserAttributeEntity uae : userEntity.getAttributes()) {
112-
LogicUtils.encryptUserAttributeEntity(session, em, uae);
100+
if (LogicUtils.isUserEncryptionEnabled(session, realmId)) {
101+
logger.debugf("Event: USER_ENCRYPTION, Realm: %s, User: %s", realmId, userId);
102+
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
103+
UserEntity userEntity = LogicUtils.getUserEntity(em, userId);
104+
LogicUtils.encryptUserEntity(session, em, userEntity);
105+
for (UserAttributeEntity uae : userEntity.getAttributes()) {
106+
LogicUtils.encryptUserAttributeEntity(session, em, uae);
107+
}
108+
em.flush();
109+
} else {
110+
logger.debugf("Event: USER_ENCRYPTION_SKIPPED, Realm: %s, User: %s", realmId, userId);
113111
}
114-
em.flush();
115112
}
116113

117114
}

src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtils.java

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import javax.crypto.spec.SecretKeySpec;
3434
import org.apache.commons.lang3.ArrayUtils;
3535
import org.jboss.logging.Logger;
36+
import org.keycloak.quarkus.runtime.configuration.Configuration;
3637

3738
/**
3839
* Provides encryption functionalities.
@@ -44,7 +45,7 @@ public final class EncryptionUtils {
4445
/**
4546
* Encryption algorithm to use.
4647
*/
47-
static String algorithm = "AES/CBC/PKCS5Padding";
48+
static final String ALGORITHM = "AES/CBC/PKCS5Padding";
4849

4950
/**
5051
* String to prefixed to encrypted value before it is stored in db to
@@ -67,7 +68,7 @@ public static String encryptValue(String value) {
6768
}
6869
byte[] iv = new byte[16];
6970
new SecureRandom().nextBytes(iv);
70-
Cipher cipher = Cipher.getInstance(algorithm);
71+
Cipher cipher = Cipher.getInstance(ALGORITHM);
7172
cipher.init(Cipher.ENCRYPT_MODE, getEncryptionKey(), new IvParameterSpec(iv));
7273
byte[] cipherText = cipher.doFinal(value.getBytes());
7374
return CIPHERTEXT_PREFIX + Base64.getEncoder().encodeToString(ArrayUtils.addAll(iv, cipherText));
@@ -95,7 +96,7 @@ public static String decryptValue(String value) {
9596
byte[] cipherTextWithIv = Base64.getDecoder().decode(value.substring(CIPHERTEXT_PREFIX.length()));
9697
byte[] iv = ArrayUtils.subarray(cipherTextWithIv, 0, 16);
9798
byte[] cipherText = ArrayUtils.subarray(cipherTextWithIv, 16, cipherTextWithIv.length);
98-
Cipher cipher = Cipher.getInstance(algorithm);
99+
Cipher cipher = Cipher.getInstance(ALGORITHM);
99100
cipher.init(Cipher.DECRYPT_MODE, getEncryptionKey(), new IvParameterSpec(iv));
100101
byte[] plainText = cipher.doFinal(cipherText);
101102
return new String(plainText);
@@ -121,8 +122,8 @@ public static boolean isEncryptedValue(String value) {
121122
}
122123

123124
/**
124-
* Gets encryption key from KC_PII_ENCKEY envvar, or generate one from
125-
* KC_DB_URL envvar.
125+
* Gets encryption key from KC_PII_ENCKEY environment variable, or generate
126+
* one from the JDBC URL of the database.
126127
*
127128
* @return SecretKey
128129
* @throws NoSuchAlgorithmException
@@ -131,22 +132,19 @@ static synchronized SecretKey getEncryptionKey() throws NoSuchAlgorithmException
131132
if (key != null) {
132133
return key;
133134
}
134-
135135
String rawkey = System.getenv("KC_PII_ENCKEY");
136136
if (rawkey == null || rawkey.isBlank()) {
137137
MessageDigest md = MessageDigest.getInstance("MD5");
138-
md.update(System.getenv("KC_DB_URL").getBytes());
138+
String dbUrl = Configuration.getRawValue("kc.db-url");
139+
if (dbUrl == null || dbUrl.isBlank()) {
140+
throw new IllegalArgumentException("Unable to generate encryption key from JDBC URL of the database. Please explicitly set the encryption key using KC_PII_ENCKEY environment variable.");
141+
}
142+
md.update(dbUrl.getBytes());
139143
rawkey = HexFormat.of().formatHex(md.digest()).toLowerCase();
140-
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.");
144+
Logger.getLogger(EncryptionUtils.class).warn(String.format("Encryption key %s was generated using MD5 hash of JDBC URL of the database. It is recommended to set this key as KC_PII_ENCKEY environment variable.", rawkey));
141145
}
142-
143146
SecretKeySpec genKey = new SecretKeySpec(rawkey.getBytes(), "AES");
144-
try {
145-
validateKey(genKey);
146-
} catch (IllegalArgumentException e) {
147-
throw e;
148-
}
149-
147+
validateKey(genKey);
150148
return key = genKey;
151149
}
152150

@@ -157,12 +155,12 @@ static synchronized SecretKey getEncryptionKey() throws NoSuchAlgorithmException
157155
*/
158156
public static void validateKey(SecretKeySpec candidateKey) {
159157
try {
160-
Cipher cipher = Cipher.getInstance(algorithm);
158+
Cipher cipher = Cipher.getInstance(ALGORITHM);
161159
cipher.init(Cipher.ENCRYPT_MODE, candidateKey, new IvParameterSpec(new byte[16]));
162160
// Trivial encryption to validate
163161
cipher.doFinal("test".getBytes(StandardCharsets.UTF_8));
164162
} catch (InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) {
165-
throw new IllegalArgumentException("Invalid encryption key for algorithm " + algorithm, e);
163+
throw new IllegalArgumentException("Invalid encryption key for algorithm " + ALGORITHM, e);
166164
}
167165
}
168166

src/main/java/my/unifi/eset/keycloak/piidataencryption/utils/LogicUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public static EncryptedUserAttributeEntity getEncryptedUserAttributeEntity(Entit
186186
*
187187
* @param ks KeycloakSession
188188
* @param em EntityManager
189-
* @param realmId The realm ID
189+
* @param realm The RealmModel to encrypt all of its users
190190
*/
191191
public static void encryptExistingUserEntities(KeycloakSession ks, EntityManager em, RealmModel realm) {
192192
List<UserEntity> realmUsers = em.createQuery("SELECT u FROM UserEntity u WHERE u.realmId = :realmId", UserEntity.class).setParameter("realmId", realm.getId()).getResultList();

src/test/java/my/unifi/eset/keycloak/piidataencryption/utils/EncryptionUtilsTest.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
import java.security.NoSuchAlgorithmException;
3131

32-
import static my.unifi.eset.keycloak.piidataencryption.utils.EncryptionUtils.algorithm;
3332
import static org.junit.jupiter.api.Assertions.*;
3433

3534
@ExtendWith(SystemStubsExtension.class)
@@ -74,8 +73,7 @@ void testWrongKeySize() {
7473
EncryptionUtils.getEncryptionKey();
7574
});
7675

77-
assertEquals("Invalid encryption key for algorithm " + algorithm, thrown.getMessage());
78-
76+
assertEquals("Invalid encryption key for algorithm " + EncryptionUtils.ALGORITHM, thrown.getMessage());
7977

8078
environmentVariables.set(envVarKey, validEncKey);
8179
assertDoesNotThrow(() -> EncryptionUtils.getEncryptionKey(), "should work once the key is correct and not reuse the previous one");
@@ -98,8 +96,7 @@ void testValidateAnInvalidKey() {
9896
EncryptionUtils.validateKey(invalidKey);
9997
});
10098

101-
String expectedMessage = "Invalid encryption key for algorithm " + algorithm;
102-
assertThrows(IllegalArgumentException.class, () -> EncryptionUtils.validateKey(invalidKey));
103-
assert(thrown.getMessage().contains(expectedMessage));
99+
String expectedMessage = "Invalid encryption key for algorithm " + EncryptionUtils.ALGORITHM;
100+
assert (thrown.getMessage().contains(expectedMessage));
104101
}
105102
}

0 commit comments

Comments
 (0)