Skip to content

Commit 3c6c74d

Browse files
committed
Resolved GH-13: user data synced from LDAP were not encrypted
1 parent aaf6358 commit 3c6c74d

File tree

5 files changed

+58
-12
lines changed

5 files changed

+58
-12
lines changed

pom.xml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>my.unifi.eset</groupId>
55
<artifactId>keycloak-pii-data-encryption</artifactId>
6-
<version>2.4</version>
6+
<version>2.5</version>
77
<packaging>jar</packaging>
88
<properties>
99
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,6 +28,12 @@
2828
<version>${keycloak.version}</version>
2929
<scope>provided</scope>
3030
</dependency>
31+
<dependency>
32+
<groupId>org.keycloak</groupId>
33+
<artifactId>keycloak-ldap-federation</artifactId>
34+
<version>${keycloak.version}</version>
35+
<scope>provided</scope>
36+
</dependency>
3137
<dependency>
3238
<groupId>org.apache.maven.plugins</groupId>
3339
<artifactId>maven-surefire-plugin</artifactId>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package my.unifi.eset.keycloak.piidataencryption.ldap;
2+
3+
import jakarta.persistence.EntityManager;
4+
import my.unifi.eset.keycloak.piidataencryption.utils.LogicUtils;
5+
import org.keycloak.component.ComponentModel;
6+
import org.keycloak.connections.jpa.JpaConnectionProvider;
7+
import org.keycloak.models.KeycloakSession;
8+
import org.keycloak.models.KeycloakSessionFactory;
9+
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
10+
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
11+
import org.keycloak.storage.user.SynchronizationResult;
12+
13+
public class EncryptedLDAPStorageProviderFactory extends LDAPStorageProviderFactory {
14+
15+
@Override
16+
public int order() {
17+
return 1000;
18+
}
19+
20+
@Override
21+
protected SynchronizationResult syncImpl(KeycloakSessionFactory sessionFactory, LDAPQuery userQuery, String realmId, ComponentModel fedModel) {
22+
SynchronizationResult result = super.syncImpl(sessionFactory, userQuery, realmId, fedModel);
23+
KeycloakSession session = sessionFactory.create();
24+
EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager();
25+
LogicUtils.encryptExistingUserEntities(session, em, session.realms().getRealm(realmId));
26+
em.flush();
27+
return result;
28+
}
29+
30+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package my.unifi.eset.keycloak.piidataencryption.listeners;
1817

1918
import my.unifi.eset.keycloak.piidataencryption.utils.EncryptionUtils;
@@ -77,7 +76,10 @@ protected void handlePreLoadEventUserEntity(PreLoadEvent ple, UserEntity ue) {
7776
if (eue != null) {
7877
Object[] states = ple.getState();
7978
Map<String, Integer> cols = collectColumnIndices(ple.getPersister().getEntityMetamodel().getPropertyNames());
80-
if (validateHashValueVsEncryptedValue((String) states[cols.get("username")], eue.getUsername())) {
79+
if (!LogicUtils.isHash((String) states[cols.get("username")])) {
80+
// skip because the entity is already decrypted
81+
logger.debugf("Event: USER_ALREADY_DECRYPTED, Realm: %s, User: %s", states[cols.get("realmId")], ue.getId());
82+
} else if (validateHashValueVsEncryptedValue((String) states[cols.get("username")], eue.getUsername())) {
8183
logger.debugf("Event: USER_DECRYPTION, Realm: %s, User: %s", states[cols.get("realmId")], ue.getId());
8284
states[cols.get("username")] = EncryptionUtils.decryptValue(eue.getUsername());
8385
states[cols.get("email")] = EncryptionUtils.decryptValue(eue.getEmail());

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package my.unifi.eset.keycloak.piidataencryption.utils;
1817

1918
import jakarta.persistence.EntityManager;
@@ -97,12 +96,16 @@ public static boolean shouldEncryptAttribute(KeycloakSession ks, String realmId,
9796
if (attributeName.startsWith("pii-")) {
9897
return true;
9998
}
100-
UserProfileProvider upp = ks.getProvider(UserProfileProvider.class);
101-
if (upp instanceof DeclarativeUserProfileProvider dup) {
102-
UPAttribute upa = dup.getConfiguration().getAttribute(attributeName);
103-
if (upa != null && upa.getValidations().containsKey(PiiDataEncryptionValidatorProvider.ID)) {
104-
return Boolean.parseBoolean(String.valueOf(upa.getValidations().get(PiiDataEncryptionValidatorProvider.ID).getOrDefault("enable", false)));
99+
try {
100+
UserProfileProvider upp = ks.getProvider(UserProfileProvider.class);
101+
if (upp instanceof DeclarativeUserProfileProvider dup) {
102+
UPAttribute upa = dup.getConfiguration().getAttribute(attributeName);
103+
if (upa != null && upa.getValidations().containsKey(PiiDataEncryptionValidatorProvider.ID)) {
104+
return Boolean.parseBoolean(String.valueOf(upa.getValidations().get(PiiDataEncryptionValidatorProvider.ID).getOrDefault("enable", false)));
105+
}
105106
}
107+
} catch (Exception ex) {
108+
return false;
106109
}
107110
return false;
108111
}
@@ -190,6 +193,7 @@ public static EncryptedUserAttributeEntity getEncryptedUserAttributeEntity(Entit
190193
*/
191194
public static void encryptExistingUserEntities(KeycloakSession ks, EntityManager em, RealmModel realm) {
192195
List<UserEntity> realmUsers = em.createQuery("SELECT u FROM UserEntity u WHERE u.realmId = :realmId", UserEntity.class).setParameter("realmId", realm.getId()).getResultList();
196+
logger.debugf("Event: REALM_USERS_ENCRYPTION, Realm: %s, Total Users: %s", realm.getId(), realmUsers.size());
193197
for (UserEntity user : realmUsers) {
194198
if (user.getServiceAccountClientLink() != null) {
195199
continue; // skip service accounts
@@ -214,9 +218,8 @@ public static void encryptUserEntity(KeycloakSession ks, EntityManager em, UserE
214218
return;
215219
}
216220
EncryptedUserEntity eue = LogicUtils.getEncryptedUserEntity(em, ue, true);
217-
if (ue.getUsername().length() == 40 && ue.getUsername().matches("^[0-9a-fA-F]+$")) {
221+
if (isHash(ue.getUsername())) {
218222
// somehow the value is already hashed so we skip it to avoid double hash/encrypt
219-
// we only need to check email because email has a specific string format
220223
return;
221224
}
222225
eue.setUsername(EncryptionUtils.encryptValue(ue.getUsername()));
@@ -249,7 +252,7 @@ public static void encryptUserEntity(KeycloakSession ks, EntityManager em, UserE
249252
public static void encryptUserAttributeEntity(KeycloakSession ks, EntityManager em, UserAttributeEntity uae) {
250253
if (shouldEncryptAttribute(ks, uae)) {
251254
String value = uae.getValue();
252-
if (value.length() == 40 && value.matches("^[0-9a-fA-F]+$")) {
255+
if (isHash(value)) {
253256
// somehow the value is already hashed so we skip it to avoid double hash/encrypt
254257
return;
255258
}
@@ -365,6 +368,10 @@ public static String hash(String raw) {
365368
return raw != null ? DigestUtils.sha1Hex(raw.trim().toLowerCase()) : null;
366369
}
367370

371+
public static boolean isHash(String value) {
372+
return value.length() == 40 && value.matches("^[0-9a-fA-F]+$");
373+
}
374+
368375
// Makes this class un-instantiatable
369376
private LogicUtils() {
370377
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
my.unifi.eset.keycloak.piidataencryption.ldap.EncryptedLDAPStorageProviderFactory

0 commit comments

Comments
 (0)