Apache Syncope: Default AES key used for internal password encryption
Description
Apache Syncope can be configured to store the user password values in the internal database with AES encryption, though this is not the default option.
When AES is configured, the default key value, hard-coded in the source code, is always used. This allows a malicious attacker, once obtained access to the internal database content, to reconstruct the original cleartext password values. This is not affecting encrypted plain attributes, whose values are also stored using AES encryption.
Users are recommended to upgrade to version 3.0.15 / 4.0.3, which fix this issue.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Default AES key in Apache Syncope allows attackers with database access to decrypt user passwords; upgrade to fixed versions.
Vulnerability
Apache Syncope, an identity management system, can be configured to store user passwords in its internal database using AES encryption. However, when AES is enabled, a default encryption key is always used. This key is hard-coded in the source code as "1abcdefghilmnopqrstuvz2!", as seen in a commit that removed it [3]. The NVD entry confirms that this default key is always used when AES encryption is configured [2].
Exploitation
An attacker who gains access to the internal database content—such as through SQL injection, backup exposure, or other means—can read the encrypted password values. Since the AES key is hard-coded and known, the attacker can reconstruct the original cleartext passwords offline without any additional authentication. The commits addressing this issue also show that the key was previously static and not configurable [1][3].
Impact
Successful exploitation leads to disclosure of user passwords in cleartext, enabling account takeover and further compromise of the system. Note that this vulnerability does not affect other encrypted plain attributes, which use a separate AES protection mechanism [2].
Mitigation
The Apache Syncope project has released versions 3.0.15 and 4.0.3, which fix the issue by making the AES key configurable and no longer hard-coded. The fix also introduces a new configuration property security.aesSecretKey [1][3]. The oss-security announcement lists all affected versions from 2.1 to 4.0.2 [4]. Users are strongly recommended to upgrade immediately.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.syncope:syncope-coreMaven | >= 4.0.0, < 4.0.3 | 4.0.3 |
org.apache.syncope:syncope-coreMaven | < 3.0.15 | 3.0.15 |
Affected products
2- Apache Software Foundation/Apache Syncopev5Range: 2.1
Patches
29d706af25d2e[SYNCOPE-1932] Improve AES management (#1243)
26 files changed · +163 −146
core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java+1 −3 modified@@ -43,12 +43,10 @@ public class AccessTokenLogic extends AbstractTransactionalLogic<AccessTokenTO> { - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected static byte[] getAuthorities() { byte[] authorities = null; try { - authorities = ENCRYPTOR.encode(POJOHelper.serialize( + authorities = Encryptor.getInstance().encode(POJOHelper.serialize( AuthContextUtils.getAuthorities()), CipherAlgorithm.AES). getBytes(); } catch (Exception e) {
core/persistence-jpa-json/src/test/resources/core-majson-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].jdbcURL=jdbc:mariadb://${DB_CONTAINER_IP}:3306/syncope?characterEncoding=UTF-8 persistence.domain[0].poolMaxActive=10
core/persistence-jpa-json/src/test/resources/core-myjson-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].jdbcURL=jdbc:mysql://${DB_CONTAINER_IP}:3306/syncope?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8 persistence.domain[0].poolMaxActive=10
core/persistence-jpa-json/src/test/resources/core-ojson-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].jdbcURL=jdbc:oracle:thin:@${DB_CONTAINER_IP}:1521/XEPDB1 #persistence.domain[0].jdbcURL=jdbc:oracle:thin:@192.168.0.176:1521/orcl
core/persistence-jpa-json/src/test/resources/core-pgjsonb-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].jdbcURL=jdbc:postgresql://${DB_CONTAINER_IP}:5432/syncope?stringtype=unspecified persistence.domain[0].poolMaxActive=10
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPALinkedAccount.java+14 −8 modified@@ -61,8 +61,6 @@ public class JPALinkedAccount extends AbstractGeneratedKeyEntity implements Link public static final String TABLE = "LinkedAccount"; - private static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - @NotNull private String connObjectKeyValue; @@ -151,7 +149,7 @@ public void setCipherAlgorithm(final CipherAlgorithm cipherAlgorithm) { throw new IllegalArgumentException("Cannot override existing cipher algorithm"); } } - + @Override public boolean canDecodeSecrets() { return this.cipherAlgorithm != null && this.cipherAlgorithm.isInvertible(); @@ -168,14 +166,22 @@ public void setEncodedPassword(final String password, final CipherAlgorithm ciph this.cipherAlgorithm = cipherAlgoritm; } + protected String encode(final String value) throws Exception { + return Encryptor.getInstance().encode( + value, + Optional.ofNullable(cipherAlgorithm). + orElseGet(() -> CipherAlgorithm.valueOf( + ApplicationContextProvider.getBeanFactory().getBean(ConfParamOps.class).get( + AuthContextUtils.getDomain(), + "password.cipher.algorithm", + CipherAlgorithm.AES.name(), + String.class)))); + } + @Override public void setPassword(final String password) { try { - this.password = ENCRYPTOR.encode(password, cipherAlgorithm == null - ? CipherAlgorithm.valueOf(ApplicationContextProvider.getBeanFactory().getBean(ConfParamOps.class). - get(AuthContextUtils.getDomain(), "password.cipher.algorithm", CipherAlgorithm.AES.name(), - String.class)) - : cipherAlgorithm); + this.password = encode(password); } catch (Exception e) { LOG.error("Could not encode password", e); this.password = null;
core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java+14 −12 modified@@ -77,8 +77,6 @@ public class JPAUser public static final String TABLE = "SyncopeUser"; - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected static final TypeReference<List<String>> TYPEREF = new TypeReference<List<String>>() { }; @@ -223,14 +221,22 @@ public void setEncodedPassword(final String password, final CipherAlgorithm ciph setMustChangePassword(false); } + protected String encode(final String value) throws Exception { + return Encryptor.getInstance().encode( + value, + Optional.ofNullable(cipherAlgorithm). + orElseGet(() -> CipherAlgorithm.valueOf( + ApplicationContextProvider.getBeanFactory().getBean(ConfParamOps.class).get( + AuthContextUtils.getDomain(), + "password.cipher.algorithm", + CipherAlgorithm.AES.name(), + String.class)))); + } + @Override public void setPassword(final String password) { try { - this.password = ENCRYPTOR.encode(password, cipherAlgorithm == null - ? CipherAlgorithm.valueOf(ApplicationContextProvider.getBeanFactory().getBean(ConfParamOps.class). - get(AuthContextUtils.getDomain(), "password.cipher.algorithm", CipherAlgorithm.AES.name(), - String.class)) - : cipherAlgorithm); + this.password = encode(password); setMustChangePassword(false); } catch (Exception e) { LOG.error("Could not encode password", e); @@ -414,11 +420,7 @@ public String getSecurityAnswer() { @Override public void setSecurityAnswer(final String securityAnswer) { try { - this.securityAnswer = ENCRYPTOR.encode(securityAnswer, cipherAlgorithm == null - ? CipherAlgorithm.valueOf(ApplicationContextProvider.getBeanFactory().getBean(ConfParamOps.class). - get(AuthContextUtils.getDomain(), "password.cipher.algorithm", CipherAlgorithm.AES.name(), - String.class)) - : cipherAlgorithm); + this.securityAnswer = encode(securityAnswer); } catch (Exception e) { LOG.error("Could not encode security answer", e); this.securityAnswer = null;
core/persistence-jpa/src/test/resources/core-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.h2.Driver
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java+6 −10 modified@@ -49,8 +49,6 @@ public class AnyTypeDataBinderImpl implements AnyTypeDataBinder { protected static final Logger LOG = LoggerFactory.getLogger(AnyTypeDataBinder.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected final SecurityProperties securityProperties; protected final AnyTypeDAO anyTypeDAO; @@ -86,15 +84,14 @@ public AnyType create(final AnyTypeTO anyTypeTO) { AccessToken accessToken = accessTokenDAO.findByOwner(AuthContextUtils.getUsername()); try { Set<SyncopeGrantedAuthority> authorities = new HashSet<>(POJOHelper.deserialize( - ENCRYPTOR.decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), + Encryptor.getInstance().decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), new TypeReference<Set<SyncopeGrantedAuthority>>() { })); added.forEach(e -> authorities.add(new SyncopeGrantedAuthority(e, SyncopeConstants.ROOT_REALM))); - accessToken.setAuthorities(ENCRYPTOR.encode( - POJOHelper.serialize(authorities), CipherAlgorithm.AES). - getBytes()); + accessToken.setAuthorities(Encryptor.getInstance().encode( + POJOHelper.serialize(authorities), CipherAlgorithm.AES).getBytes()); accessTokenDAO.save(accessToken); } catch (Exception e) { @@ -142,16 +139,15 @@ public AnyTypeTO delete(final AnyType anyType) { AccessToken accessToken = accessTokenDAO.findByOwner(AuthContextUtils.getUsername()); try { Set<SyncopeGrantedAuthority> authorities = new HashSet<>(POJOHelper.deserialize( - ENCRYPTOR.decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), + Encryptor.getInstance().decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), new TypeReference<Set<SyncopeGrantedAuthority>>() { })); authorities.removeAll(authorities.stream(). filter(authority -> removed.contains(authority.getAuthority())).collect(Collectors.toList())); - accessToken.setAuthorities(ENCRYPTOR.encode( - POJOHelper.serialize(authorities), CipherAlgorithm.AES). - getBytes()); + accessToken.setAuthorities(Encryptor.getInstance().encode( + POJOHelper.serialize(authorities), CipherAlgorithm.AES).getBytes()); accessTokenDAO.save(accessToken); } catch (Exception e) {
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultMappingManager.java+1 −3 modified@@ -110,8 +110,6 @@ public class DefaultMappingManager implements MappingManager { protected static final Logger LOG = LoggerFactory.getLogger(MappingManager.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected final AnyTypeDAO anyTypeDAO; protected final UserDAO userDAO; @@ -500,7 +498,7 @@ public Pair<String, Set<Attribute>> prepareAttrsFromRealm(final Realm realm, fin protected String decodePassword(final Account account) { try { - return ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm()); + return Encryptor.getInstance().decode(account.getPassword(), account.getCipherAlgorithm()); } catch (Exception e) { LOG.error("Could not decode password for {}", account, e); return null;
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java+1 −3 modified@@ -68,8 +68,6 @@ public class ConnObjectUtils { protected static final Logger LOG = LoggerFactory.getLogger(ConnObjectUtils.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - public static SyncToken toSyncToken(final String syncToken) { return Optional.ofNullable(syncToken).map(st -> POJOHelper.deserialize(st, SyncToken.class)).orElse(null); } @@ -266,7 +264,7 @@ public <U extends AnyUR> U getAnyUR( // update password if and only if password is really changed User user = userDAO.authFind(key); if (StringUtils.isBlank(updatedUser.getPassword()) - || ENCRYPTOR.verify(updatedUser.getPassword(), + || Encryptor.getInstance().verify(updatedUser.getPassword(), user.getCipherAlgorithm(), user.getPassword())) { updatedUser.setPassword(null);
core/self-keymaster-starter/src/test/resources/core-debug.properties+1 −1 modified@@ -33,7 +33,7 @@ spring.h2.console.path=/h2 security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.h2.Driver
core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultPasswordRule.java+1 −3 modified@@ -59,8 +59,6 @@ public class DefaultPasswordRule implements PasswordRule { protected static final Logger LOG = LoggerFactory.getLogger(DefaultPasswordRule.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - public static List<Rule> conf2Rules(final DefaultPasswordRuleConf conf) { List<Rule> rules = new ArrayList<>(); @@ -205,7 +203,7 @@ public void enforce(final LinkedAccount account) { String clear = null; if (account.canDecodeSecrets()) { try { - clear = ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm()); + clear = Encryptor.getInstance().decode(account.getPassword(), account.getCipherAlgorithm()); } catch (Exception e) { LOG.error("Could not decode password for {}", account, e); }
core/spring/src/main/java/org/apache/syncope/core/spring/policy/HaveIBeenPwnedPasswordRule.java+4 −13 modified@@ -18,15 +18,10 @@ */ package org.apache.syncope.core.spring.policy; -import java.io.UnsupportedEncodingException; import java.net.URI; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; +import java.security.GeneralSecurityException; import java.util.Optional; import java.util.stream.Stream; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.policy.HaveIBeenPwnedPasswordRuleConf; import org.apache.syncope.common.lib.policy.PasswordRuleConf; @@ -51,8 +46,6 @@ public class HaveIBeenPwnedPasswordRule implements PasswordRule { protected static final Logger LOG = LoggerFactory.getLogger(HaveIBeenPwnedPasswordRule.class); - private static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - private HaveIBeenPwnedPasswordRuleConf conf; @Override @@ -72,7 +65,7 @@ public void setConf(final PasswordRuleConf conf) { protected void enforce(final String clearPassword) { try { - String sha1 = ENCRYPTOR.encode(clearPassword, CipherAlgorithm.SHA1); + String sha1 = Encryptor.getInstance().encode(clearPassword, CipherAlgorithm.SHA1); HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.USER_AGENT, "Apache Syncope"); @@ -88,9 +81,7 @@ protected void enforce(final String clearPassword) { throw new PasswordPolicyException("Password pwned"); } } - } catch (UnsupportedEncodingException | InvalidKeyException | NoSuchAlgorithmException - | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) { - + } catch (GeneralSecurityException e) { LOG.error("Could not encode the password value as SHA1", e); } catch (HttpStatusCodeException e) { LOG.error("Error while contacting the PwnedPasswords service", e); @@ -115,7 +106,7 @@ public void enforce(final LinkedAccount account) { String clearPassword = null; if (account.canDecodeSecrets()) { try { - clearPassword = ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm()); + clearPassword = Encryptor.getInstance().decode(account.getPassword(), account.getCipherAlgorithm()); } catch (Exception e) { LOG.error("Could not decode password for {}", account, e); }
core/spring/src/main/java/org/apache/syncope/core/spring/security/AuthDataAccessor.java+1 −3 modified@@ -83,8 +83,6 @@ public class AuthDataAccessor { public static final String GROUP_OWNER_ROLE = "GROUP_OWNER"; - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected static final Set<SyncopeGrantedAuthority> ANONYMOUS_AUTHORITIES = Set.of(new SyncopeGrantedAuthority(IdRepoEntitlement.ANONYMOUS)); @@ -255,7 +253,7 @@ public Triple<User, Boolean, String> authenticate(final String domain, final Aut } protected boolean authenticate(final User user, final String password) { - boolean authenticated = ENCRYPTOR.verify(password, user.getCipherAlgorithm(), user.getPassword()); + boolean authenticated = Encryptor.getInstance().verify(password, user.getCipherAlgorithm(), user.getPassword()); LOG.debug("{} authenticated on internal storage: {}", user.getUsername(), authenticated); for (Iterator<? extends ExternalResource> itor = getPassthroughResources(user).iterator();
core/spring/src/main/java/org/apache/syncope/core/spring/security/Encryptor.java+79 −54 modified@@ -18,19 +18,18 @@ */ package org.apache.syncope.core.spring.security; -import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.spring.ApplicationContextProvider; @@ -46,61 +45,94 @@ public final class Encryptor { private static final Map<String, Encryptor> INSTANCES = new ConcurrentHashMap<>(); - private static final String DEFAULT_SECRET_KEY = "1abcdefghilmnopqrstuvz2!"; - public static Encryptor getInstance() { return getInstance(null); } - public static Encryptor getInstance(final String secretKey) { - String actualKey = StringUtils.isBlank(secretKey) ? DEFAULT_SECRET_KEY : secretKey; - - Encryptor instance = INSTANCES.get(actualKey); - if (instance == null) { - instance = new Encryptor(actualKey); - INSTANCES.put(actualKey, instance); - } - - return instance; + public static Encryptor getInstance(final String aesSecretKey) { + SecurityProperties securityProperties = Optional.ofNullable(ApplicationContextProvider.getApplicationContext()). + flatMap(ctx -> { + try { + return Optional.ofNullable(ctx.getBean(SecurityProperties.class)); + } catch (Exception e) { + return Optional.empty(); + } + }). + orElseGet(() -> { + SecurityProperties props = new SecurityProperties(); + props.setAesSecretKey(StringUtils.EMPTY); + return props; + }); + + String actualKey = StringUtils.isBlank(aesSecretKey) ? securityProperties.getAesSecretKey() : aesSecretKey; + return INSTANCES.computeIfAbsent(actualKey, k -> new Encryptor(k, securityProperties.getDigester())); } + private final SecurityProperties.DigesterProperties digesterProperties; + private final Map<CipherAlgorithm, StandardStringDigester> digesters = new ConcurrentHashMap<>(); - private SecretKeySpec keySpec; + private final Optional<SecretKeySpec> aesKeySpec; - private Encryptor(final String secretKey) { - String actualKey = secretKey; - if (actualKey.length() < 16) { - StringBuilder actualKeyPadding = new StringBuilder(actualKey); - int length = 16 - actualKey.length(); - String randomChars = SecureRandomUtils.generateRandomPassword(length); + private Encryptor( + final String aesSecretKey, + final SecurityProperties.DigesterProperties digesterProperties) { - actualKeyPadding.append(randomChars); - actualKey = actualKeyPadding.toString(); - LOG.warn("The secret key is too short (< 16), adding some random characters. " - + "Passwords encrypted with AES and this key will not be recoverable " - + "as a result if the container is restarted."); - } + this.digesterProperties = digesterProperties; - try { - keySpec = new SecretKeySpec(ArrayUtils.subarray( - actualKey.getBytes(StandardCharsets.UTF_8), 0, 16), - CipherAlgorithm.AES.getAlgorithm()); - } catch (Exception e) { - LOG.error("Error during key specification", e); + SecretKeySpec sks = null; + + if (StringUtils.isNotBlank(aesSecretKey)) { + String actualKey = aesSecretKey; + + Integer pad = null; + boolean truncate = false; + if (actualKey.length() < 16) { + pad = 16 - actualKey.length(); + } else if (actualKey.length() > 16 && actualKey.length() < 24) { + pad = 24 - actualKey.length(); + } else if (actualKey.length() > 24 && actualKey.length() < 32) { + pad = 32 - actualKey.length(); + } else if (actualKey.length() > 32) { + truncate = true; + } + + if (pad != null) { + StringBuilder actualKeyPadding = new StringBuilder(actualKey); + String randomChars = SecureRandomUtils.generateRandomPassword(pad); + + actualKeyPadding.append(randomChars); + actualKey = actualKeyPadding.toString(); + LOG.warn("The configured AES secret key is too short (< {}), padding with random chars: {}", + actualKey.length(), actualKey); + } + if (truncate) { + actualKey = actualKey.substring(0, 32); + LOG.warn("The configured AES secret key is too long (> 32), truncating: {}", actualKey); + } + + try { + sks = new SecretKeySpec(actualKey.getBytes(StandardCharsets.UTF_8), CipherAlgorithm.AES.getAlgorithm()); + LOG.debug("AES-{} successfully configured", actualKey.length() * 8); + } catch (Exception e) { + LOG.error("Error during key specification", e); + } } + + aesKeySpec = Optional.ofNullable(sks); } public String encode(final String value, final CipherAlgorithm cipherAlgorithm) - throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { String encoded = null; if (value != null) { if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) { Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); - cipher.init(Cipher.ENCRYPT_MODE, keySpec); + cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec. + orElseThrow(() -> new IllegalArgumentException("AES not configured"))); encoded = Base64.getEncoder().encodeToString(cipher.doFinal(value.getBytes(StandardCharsets.UTF_8))); } else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) { @@ -134,14 +166,15 @@ public boolean verify(final String value, final CipherAlgorithm cipherAlgorithm, } public String decode(final String encoded, final CipherAlgorithm cipherAlgorithm) - throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { String decoded = null; if (encoded != null && cipherAlgorithm == CipherAlgorithm.AES) { Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, keySpec); + cipher.init(Cipher.DECRYPT_MODE, aesKeySpec. + orElseThrow(() -> new IllegalArgumentException("AES not configured"))); decoded = new String(cipher.doFinal(Base64.getDecoder().decode(encoded)), StandardCharsets.UTF_8); } @@ -150,24 +183,19 @@ public String decode(final String encoded, final CipherAlgorithm cipherAlgorithm } private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm) { - StandardStringDigester digester = digesters.get(cipherAlgorithm); - if (digester == null) { - digester = new StandardStringDigester(); + return digesters.computeIfAbsent(cipherAlgorithm, k -> { + StandardStringDigester digester = new StandardStringDigester(); if (cipherAlgorithm.getAlgorithm().startsWith("S-")) { - SecurityProperties securityProperties = - ApplicationContextProvider.getApplicationContext().getBean(SecurityProperties.class); - // Salted ... digester.setAlgorithm(cipherAlgorithm.getAlgorithm().replaceFirst("S\\-", "")); - digester.setIterations(securityProperties.getDigester().getSaltIterations()); - digester.setSaltSizeBytes(securityProperties.getDigester().getSaltSizeBytes()); + digester.setIterations(digesterProperties.getSaltIterations()); + digester.setSaltSizeBytes(digesterProperties.getSaltSizeBytes()); digester.setInvertPositionOfPlainSaltInEncryptionResults( - securityProperties.getDigester().isInvertPositionOfPlainSaltInEncryptionResults()); + digesterProperties.isInvertPositionOfPlainSaltInEncryptionResults()); digester.setInvertPositionOfSaltInMessageBeforeDigesting( - securityProperties.getDigester().isInvertPositionOfSaltInMessageBeforeDigesting()); - digester.setUseLenientSaltSizeCheck( - securityProperties.getDigester().isUseLenientSaltSizeCheck()); + digesterProperties.isInvertPositionOfSaltInMessageBeforeDigesting()); + digester.setUseLenientSaltSizeCheck(digesterProperties.isUseLenientSaltSizeCheck()); } else { // Not salted ... digester.setAlgorithm(cipherAlgorithm.getAlgorithm()); @@ -176,10 +204,7 @@ private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm } digester.setStringOutputType(CommonUtils.STRING_OUTPUT_TYPE_HEXADECIMAL); - - digesters.put(cipherAlgorithm, digester); - } - - return digester; + return digester; + }); } }
core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java+3 −4 modified@@ -27,6 +27,7 @@ import java.io.Reader; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import java.util.Optional; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; @@ -63,10 +64,8 @@ public static GrantedAuthorityDefaults grantedAuthorityDefaults() { } protected static String jwsKey(final JWSAlgorithm jwsAlgorithm, final SecurityProperties props) { - String jwsKey = props.getJwsKey(); - if (jwsKey == null) { - throw new IllegalArgumentException("No JWS key provided"); - } + String jwsKey = Optional.ofNullable(props.getJwsKey()). + orElseThrow(() -> new IllegalArgumentException("No JWS key provided")); if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) { int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256)
core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java+5 −5 modified@@ -98,7 +98,7 @@ public void setUseLenientSaltSizeCheck(final boolean useLenientSaltSizeCheck) { private String jwsAlgorithm = JWSAlgorithm.HS512.getName(); - private String secretKey; + private String aesSecretKey; private String groovyBlacklist = "classpath:META-INF/groovy.blacklist"; @@ -168,12 +168,12 @@ public void setJwsAlgorithm(final String jwsAlgorithm) { this.jwsAlgorithm = jwsAlgorithm; } - public String getSecretKey() { - return secretKey; + public String getAesSecretKey() { + return aesSecretKey; } - public void setSecretKey(final String secretKey) { - this.secretKey = secretKey; + public void setAesSecretKey(final String aesSecretKey) { + this.aesSecretKey = aesSecretKey; } public String getGroovyBlacklist() {
core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java+1 −3 modified@@ -47,8 +47,6 @@ public class SyncopeJWTSSOProvider implements JWTSSOProvider { protected static final Logger LOG = LoggerFactory.getLogger(SyncopeJWTSSOProvider.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected final SecurityProperties securityProperties; protected final AccessTokenJWSVerifier delegate; @@ -104,7 +102,7 @@ public Pair<User, Set<SyncopeGrantedAuthority>> resolve(final JWTClaimsSet jwtCl if (accessToken.getAuthorities() != null) { try { authorities = POJOHelper.deserialize( - ENCRYPTOR.decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), + Encryptor.getInstance().decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), new TypeReference<>() { }); } catch (Throwable t) {
core/spring/src/main/java/org/apache/syncope/core/spring/security/UsernamePasswordAuthenticationProvider.java+2 −4 modified@@ -42,8 +42,6 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro protected static final Logger LOG = LoggerFactory.getLogger(UsernamePasswordAuthenticationProvider.class); - protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); - protected final DomainOps domainOps; protected final AuthDataAccessor dataAccessor; @@ -97,12 +95,12 @@ public Authentication authenticate(final Authentication authentication) { username.set(securityProperties.getAdminUser()); if (SyncopeConstants.MASTER_DOMAIN.equals(domain.getKey())) { credentialChecker.checkIsDefaultAdminPasswordInUse(); - authenticated = ENCRYPTOR.verify( + authenticated = Encryptor.getInstance().verify( authentication.getCredentials().toString(), securityProperties.getAdminPasswordAlgorithm(), securityProperties.getAdminPassword()); } else { - authenticated = ENCRYPTOR.verify( + authenticated = Encryptor.getInstance().verify( authentication.getCredentials().toString(), domain.getAdminCipherAlgorithm(), domain.getAdminPassword());
core/spring/src/test/java/org/apache/syncope/core/spring/security/EncryptorTest.java+3 −1 modified@@ -36,7 +36,9 @@ public class EncryptorTest { @BeforeAll public static void setUp() { - ApplicationContextProvider.getBeanFactory().registerSingleton("securityProperties", new SecurityProperties()); + SecurityProperties props = new SecurityProperties(); + props.setAesSecretKey("1abcdefghilmnopq"); + ApplicationContextProvider.getBeanFactory().registerSingleton("securityProperties", props); ENCRYPTOR = Encryptor.getInstance(); }
core/starter/src/main/resources/core.properties+8 −1 modified@@ -96,7 +96,14 @@ security.jwtIssuer=ApacheSyncope security.jwsAlgorithm=HS512 security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +# Key length drives AES algorithm variant selection: +# +# * 16 chars => AES-128 +# * 24 chars => AES-192 +# * 32 chars => AES-256 +# +# Shorter keys will be padded to the nearest longer option available; keys > 32 will be trucated +security.aesSecretKey=${secretKey} # default for LDAP / RFC2307 SSHA security.digester.saltIterations=1
fit/core-reference/src/main/resources/core-embedded.properties+1 −1 modified@@ -40,7 +40,7 @@ spring.datasource.driver-class-name=org.h2.Driver security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=NyefOIpekEJVBASbbETMbcns11HouPNn persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.h2.Driver
fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java+2 −2 modified@@ -330,8 +330,8 @@ public void domainUpdateAdminPassword() throws Exception { // 1. change admin pwd for domain Two domainOps.changeAdminPassword( two.getKey(), - Encryptor.getInstance().encode("password3", CipherAlgorithm.AES), - CipherAlgorithm.AES); + Encryptor.getInstance().encode("password3", CipherAlgorithm.BCRYPT), + CipherAlgorithm.BCRYPT); // 2. attempt to access with old pwd -> fail try {
pom.xml+1 −1 modified@@ -1710,7 +1710,7 @@ under the License. <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> - <version>3.4.2</version> + <version>3.5.0</version> </plugin> <plugin>
src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc+9 −6 modified@@ -24,13 +24,16 @@ Most run-time configuration options are available as parameters and can be tuned * `password.cipher.algorithm` - which cipher algorithm shall be used for encrypting password values; supported algorithms include `SHA-1`, `SHA-256`, `SHA-512`, `AES`, `S-MD5`, `S-SHA-1`, `S-SHA-256`, `S-SHA-512` and `BCRYPT`; salting options are available in the `core.properties` file; +* `security.aesSecretKey` - used for AES-based encryption / decryption: besides password values, this is also used +whenever reversible encryption is needed, throughout the whole system; [WARNING] -The value of the `security.secretKey` property in the `core.properties` file is used for AES-based encryption / decryption. -Besides password values, this is also used whenever reversible encryption is needed, throughout the whole system. + -When the `secretKey` value has length less than 16, it is right-padded by random characters during startup, to reach -such mininum value. + -It is *strongly* recommended to provide a value long at least 16 characters, in order to avoid unexpected behaviors -at runtime, expecially with high-availability. +The actual length of the `security.aesSecretKey` value is used to drive the AES algorithm variant selection: +16 characters implies `AES-128`, 24 selects `AES-192` and 32 configures `AES-256`. + +When the `security.aesSecretKey` value has length less than 16, between 17 and 23 or between 25 and 31, it is +right-padded by random characters during startup, to reach the nearest option. If the specified value is instead longer +than 32 characters, it is truncated to 32. + +It is *strongly* recommended to provide a value long exactly 16, 24 or 32 characters, in order to avoid unexpected +behaviors at runtime, expecially with high-availability. * `jwt.lifetime.minutes` - validity of https://en.wikipedia.org/wiki/JSON_Web_Token[JSON Web Token^] values used for <<rest-authentication-and-authorization,authentication>> (in minutes); * `notificationjob.cronExpression` -
297498ebfc86[SYNCOPE-1932] Improve AES management (#1241)
22 files changed · +147 −93
core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java+2 −3 modified@@ -63,9 +63,8 @@ public AccessTokenLogic( protected byte[] getAuthorities() { byte[] authorities = null; try { - authorities = encryptorManager.getInstance().encode(POJOHelper.serialize( - AuthContextUtils.getAuthorities()), CipherAlgorithm.AES). - getBytes(); + authorities = encryptorManager.getInstance(). + encode(POJOHelper.serialize(AuthContextUtils.getAuthorities()), CipherAlgorithm.AES).getBytes(); } catch (Exception e) { LOG.error("Could not fetch authorities", e); }
core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java+4 −1 modified@@ -20,6 +20,7 @@ import jakarta.persistence.EntityManagerFactory; import javax.sql.DataSource; +import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.keymaster.client.api.ConfParamOps; import org.apache.syncope.common.keymaster.client.api.DomainOps; import org.apache.syncope.common.keymaster.client.api.model.JPADomain; @@ -115,7 +116,9 @@ public ConnectorManager connectorManager() { @Bean public EncryptorManager encryptorManager() { - return new DefaultEncryptorManager(); + SecurityProperties securityProperties = new SecurityProperties(); + securityProperties.setAesSecretKey(StringUtils.EMPTY); + return new DefaultEncryptorManager(securityProperties); } @Bean
core/persistence-jpa/src/test/resources/core-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.db-type=${DB_TYPE}
core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java+4 −1 modified@@ -20,6 +20,7 @@ import javax.cache.CacheManager; import javax.cache.Caching; +import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.keymaster.client.api.ConfParamOps; import org.apache.syncope.common.keymaster.client.api.DomainOps; import org.apache.syncope.common.keymaster.client.api.model.Neo4jDomain; @@ -107,7 +108,9 @@ public ConnectorManager connectorManager() { @Bean public EncryptorManager encryptorManager() { - return new DefaultEncryptorManager(); + SecurityProperties securityProperties = new SecurityProperties(); + securityProperties.setAesSecretKey(StringUtils.EMPTY); + return new DefaultEncryptorManager(securityProperties); } @Bean
core/persistence-neo4j/src/test/resources/core-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].key=Master persistence.domain[0].uri=bolt://${NEO4J_CONTAINER_IP}:7687/
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyTypeDataBinderImpl.java+6 −8 modified@@ -96,9 +96,8 @@ public AnyType create(final AnyTypeTO anyTypeTO) { added.forEach(e -> authorities.add(new SyncopeGrantedAuthority(e, SyncopeConstants.ROOT_REALM))); - accessToken.setAuthorities(encryptorManager.getInstance().encode( - POJOHelper.serialize(authorities), CipherAlgorithm.AES). - getBytes()); + accessToken.setAuthorities(encryptorManager.getInstance(). + encode(POJOHelper.serialize(authorities), CipherAlgorithm.AES).getBytes()); accessTokenDAO.save(accessToken); } catch (Exception e) { @@ -142,17 +141,16 @@ public AnyTypeTO delete(final AnyType anyType) { orElseThrow(() -> new NotFoundException("AccessToken for " + AuthContextUtils.getUsername())); try { Set<SyncopeGrantedAuthority> authorities = new HashSet<>(POJOHelper.deserialize( - encryptorManager.getInstance().decode( - new String(accessToken.getAuthorities()), CipherAlgorithm.AES), + encryptorManager.getInstance(). + decode(new String(accessToken.getAuthorities()), CipherAlgorithm.AES), new TypeReference<Set<SyncopeGrantedAuthority>>() { })); authorities.removeAll(authorities.stream(). filter(authority -> removed.contains(authority.getAuthority())).toList()); - accessToken.setAuthorities(encryptorManager.getInstance().encode( - POJOHelper.serialize(authorities), CipherAlgorithm.AES). - getBytes()); + accessToken.setAuthorities(encryptorManager.getInstance(). + encode(POJOHelper.serialize(authorities), CipherAlgorithm.AES).getBytes()); accessTokenDAO.save(accessToken); } catch (Exception e) {
core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java+2 −2 modified@@ -269,8 +269,8 @@ public <U extends AnyUR> U getAnyUR( // update password if and only if password is really changed User user = userDAO.authFind(key); if (StringUtils.isBlank(updatedUser.getPassword()) - || encryptorManager.getInstance().verify(updatedUser.getPassword(), - user.getCipherAlgorithm(), user.getPassword())) { + || encryptorManager.getInstance(). + verify(updatedUser.getPassword(), user.getCipherAlgorithm(), user.getPassword())) { updatedUser.setPassword(null); }
core/provisioning-java/src/test/resources/core-test.properties+1 −1 modified@@ -18,7 +18,7 @@ security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.postgresql.Driver
core/self-keymaster-starter/src/test/resources/core-debug.properties+1 −1 modified@@ -29,7 +29,7 @@ keymaster.password=${anonymousKey} security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=${secretKey} persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.postgresql.Driver
core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java+61 −42 modified@@ -23,15 +23,15 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; -import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.syncope.common.lib.types.CipherAlgorithm; -import org.apache.syncope.core.persistence.api.ApplicationContextProvider; import org.apache.syncope.core.persistence.api.Encryptor; import org.jasypt.commons.CommonUtils; import org.jasypt.digest.StandardStringDigester; @@ -43,33 +43,58 @@ public class DefaultEncryptor implements Encryptor { protected static final Logger LOG = LoggerFactory.getLogger(DefaultEncryptor.class); - protected static final String DEFAULT_SECRET_KEY = "1abcdefghilmnopqrstuvz2!"; + protected final SecurityProperties.DigesterProperties digesterProperties; protected final Map<CipherAlgorithm, StandardStringDigester> digesters = new ConcurrentHashMap<>(); - protected SecretKeySpec keySpec; + protected final Optional<SecretKeySpec> aesKeySpec; - protected DefaultEncryptor(final String secretKey) { - String actualKey = secretKey; - if (actualKey.length() < 16) { - StringBuilder actualKeyPadding = new StringBuilder(actualKey); - int length = 16 - actualKey.length(); - String randomChars = SecureRandomUtils.generateRandomPassword(length); + protected DefaultEncryptor( + final String aesSecretKey, + final SecurityProperties.DigesterProperties digesterProperties) { - actualKeyPadding.append(randomChars); - actualKey = actualKeyPadding.toString(); - LOG.warn("The secret key is too short (< 16), adding some random characters. " - + "Passwords encrypted with AES and this key will not be recoverable " - + "as a result if the container is restarted."); - } + this.digesterProperties = digesterProperties; - try { - keySpec = new SecretKeySpec(ArrayUtils.subarray( - actualKey.getBytes(StandardCharsets.UTF_8), 0, 16), - CipherAlgorithm.AES.getAlgorithm()); - } catch (Exception e) { - LOG.error("Error during key specification", e); + SecretKeySpec sks = null; + + if (StringUtils.isNotBlank(aesSecretKey)) { + String actualKey = aesSecretKey; + + Integer pad = null; + boolean truncate = false; + if (actualKey.length() < 16) { + pad = 16 - actualKey.length(); + } else if (actualKey.length() > 16 && actualKey.length() < 24) { + pad = 24 - actualKey.length(); + } else if (actualKey.length() > 24 && actualKey.length() < 32) { + pad = 32 - actualKey.length(); + } else if (actualKey.length() > 32) { + truncate = true; + } + + if (pad != null) { + StringBuilder actualKeyPadding = new StringBuilder(actualKey); + String randomChars = SecureRandomUtils.generateRandomPassword(pad); + + actualKeyPadding.append(randomChars); + actualKey = actualKeyPadding.toString(); + LOG.warn("The configured AES secret key is too short (< {}), padding with random chars: {}", + actualKey.length(), actualKey); + } + if (truncate) { + actualKey = actualKey.substring(0, 32); + LOG.warn("The configured AES secret key is too long (> 32), truncating: {}", actualKey); + } + + try { + sks = new SecretKeySpec(actualKey.getBytes(StandardCharsets.UTF_8), CipherAlgorithm.AES.getAlgorithm()); + LOG.debug("AES-{} successfully configured", actualKey.length() * 8); + } catch (Exception e) { + LOG.error("Error during key specification", e); + } } + + aesKeySpec = Optional.ofNullable(sks); } @Override @@ -82,7 +107,8 @@ public String encode(final String value, final CipherAlgorithm cipherAlgorithm) if (value != null) { if (cipherAlgorithm == null || cipherAlgorithm == CipherAlgorithm.AES) { Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); - cipher.init(Cipher.ENCRYPT_MODE, keySpec); + cipher.init(Cipher.ENCRYPT_MODE, aesKeySpec. + orElseThrow(() -> new IllegalArgumentException("AES not configured"))); encoded = Base64.getEncoder().encodeToString(cipher.doFinal(value.getBytes(StandardCharsets.UTF_8))); } else if (cipherAlgorithm == CipherAlgorithm.BCRYPT) { @@ -125,33 +151,29 @@ public String decode(final String encoded, final CipherAlgorithm cipherAlgorithm if (encoded != null && cipherAlgorithm == CipherAlgorithm.AES) { Cipher cipher = Cipher.getInstance(CipherAlgorithm.AES.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, keySpec); + cipher.init(Cipher.DECRYPT_MODE, aesKeySpec. + orElseThrow(() -> new IllegalArgumentException("AES not configured"))); decoded = new String(cipher.doFinal(Base64.getDecoder().decode(encoded)), StandardCharsets.UTF_8); } return decoded; } - private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm) { - StandardStringDigester digester = digesters.get(cipherAlgorithm); - if (digester == null) { - digester = new StandardStringDigester(); + protected StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm) { + return digesters.computeIfAbsent(cipherAlgorithm, k -> { + StandardStringDigester digester = new StandardStringDigester(); if (cipherAlgorithm.getAlgorithm().startsWith("S-")) { - SecurityProperties securityProperties = - ApplicationContextProvider.getApplicationContext().getBean(SecurityProperties.class); - // Salted ... digester.setAlgorithm(cipherAlgorithm.getAlgorithm().replaceFirst("S\\-", "")); - digester.setIterations(securityProperties.getDigester().getSaltIterations()); - digester.setSaltSizeBytes(securityProperties.getDigester().getSaltSizeBytes()); + digester.setIterations(digesterProperties.getSaltIterations()); + digester.setSaltSizeBytes(digesterProperties.getSaltSizeBytes()); digester.setInvertPositionOfPlainSaltInEncryptionResults( - securityProperties.getDigester().isInvertPositionOfPlainSaltInEncryptionResults()); + digesterProperties.isInvertPositionOfPlainSaltInEncryptionResults()); digester.setInvertPositionOfSaltInMessageBeforeDigesting( - securityProperties.getDigester().isInvertPositionOfSaltInMessageBeforeDigesting()); - digester.setUseLenientSaltSizeCheck( - securityProperties.getDigester().isUseLenientSaltSizeCheck()); + digesterProperties.isInvertPositionOfSaltInMessageBeforeDigesting()); + digester.setUseLenientSaltSizeCheck(digesterProperties.isUseLenientSaltSizeCheck()); } else { // Not salted ... digester.setAlgorithm(cipherAlgorithm.getAlgorithm()); @@ -160,10 +182,7 @@ private StandardStringDigester getDigester(final CipherAlgorithm cipherAlgorithm } digester.setStringOutputType(CommonUtils.STRING_OUTPUT_TYPE_HEXADECIMAL); - - digesters.put(cipherAlgorithm, digester); - } - - return digester; + return digester; + }); } }
core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java+9 −3 modified@@ -26,16 +26,22 @@ public class DefaultEncryptorManager implements EncryptorManager { + protected final SecurityProperties securityProperties; + protected final Map<String, DefaultEncryptor> instances = new ConcurrentHashMap<>(); + public DefaultEncryptorManager(final SecurityProperties securityProperties) { + this.securityProperties = securityProperties; + } + @Override public Encryptor getInstance() { return getInstance(null); } @Override - public Encryptor getInstance(final String secretKey) { - String actualKey = StringUtils.isBlank(secretKey) ? DefaultEncryptor.DEFAULT_SECRET_KEY : secretKey; - return instances.computeIfAbsent(actualKey, DefaultEncryptor::new); + public Encryptor getInstance(final String aesSecretKey) { + String actualKey = StringUtils.isBlank(aesSecretKey) ? securityProperties.getAesSecretKey() : aesSecretKey; + return instances.computeIfAbsent(actualKey, k -> new DefaultEncryptor(k, securityProperties.getDigester())); } }
core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java+5 −6 modified@@ -27,6 +27,7 @@ import java.io.Reader; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import java.util.Optional; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.ApplicationContextProvider; import org.apache.syncope.core.persistence.api.EncryptorManager; @@ -64,10 +65,8 @@ public static GrantedAuthorityDefaults grantedAuthorityDefaults() { } protected static String jwsKey(final JWSAlgorithm jwsAlgorithm, final SecurityProperties props) { - String jwsKey = props.getJwsKey(); - if (jwsKey == null) { - throw new IllegalArgumentException("No JWS key provided"); - } + String jwsKey = Optional.ofNullable(props.getJwsKey()). + orElseThrow(() -> new IllegalArgumentException("No JWS key provided")); if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) { int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256) @@ -158,8 +157,8 @@ public ApplicationContextProvider applicationContextProvider() { } @Bean - public EncryptorManager encryptorManager() { - return new DefaultEncryptorManager(); + public EncryptorManager encryptorManager(final SecurityProperties securityProperties) { + return new DefaultEncryptorManager(securityProperties); } @ConditionalOnMissingBean
core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java+5 −5 modified@@ -98,7 +98,7 @@ public void setUseLenientSaltSizeCheck(final boolean useLenientSaltSizeCheck) { private String jwsAlgorithm = JWSAlgorithm.HS512.getName(); - private String secretKey; + private String aesSecretKey; private String groovyBlacklist = "classpath:META-INF/groovy.blacklist"; @@ -168,12 +168,12 @@ public void setJwsAlgorithm(final String jwsAlgorithm) { this.jwsAlgorithm = jwsAlgorithm; } - public String getSecretKey() { - return secretKey; + public String getAesSecretKey() { + return aesSecretKey; } - public void setSecretKey(final String secretKey) { - this.secretKey = secretKey; + public void setAesSecretKey(final String secretKey) { + this.aesSecretKey = secretKey; } public String getGroovyBlacklist() {
core/spring/src/test/java/org/apache/syncope/core/spring/security/DefaultEncryptorTest.java+9 −3 modified@@ -26,6 +26,7 @@ import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.ApplicationContextProvider; import org.apache.syncope.core.persistence.api.Encryptor; +import org.apache.syncope.core.spring.SpringTestConfiguration; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -37,8 +38,13 @@ public class DefaultEncryptorTest { @BeforeAll public static void setUp() { - ApplicationContextProvider.getBeanFactory().registerSingleton("securityProperties", new SecurityProperties()); - ENCRYPTOR = new DefaultEncryptorManager().getInstance(); + SecurityProperties props = new SecurityProperties(); + props.setAesSecretKey(SpringTestConfiguration.AES_SECRET_KEY); + ApplicationContextProvider.getBeanFactory().registerSingleton("securityProperties", props); + + SecurityProperties securityProperties = new SecurityProperties(); + securityProperties.setAesSecretKey(SpringTestConfiguration.AES_SECRET_KEY); + ENCRYPTOR = new DefaultEncryptorManager(securityProperties).getInstance(); } @Test @@ -68,7 +74,7 @@ public void decodeDefaultAESKey() throws Exception { @Test public void smallKey() throws Exception { - DefaultEncryptor smallKeyEncryptor = new DefaultEncryptor("123"); + DefaultEncryptor smallKeyEncryptor = new DefaultEncryptor("123", new SecurityProperties().getDigester()); String encPassword = smallKeyEncryptor.encode(PASSWORD_VALUE, CipherAlgorithm.AES); String decPassword = smallKeyEncryptor.decode(encPassword, CipherAlgorithm.AES); assertEquals(PASSWORD_VALUE, decPassword);
core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java+6 −1 modified@@ -27,6 +27,7 @@ import org.apache.syncope.core.provisioning.api.ImplementationLookup; import org.apache.syncope.core.spring.security.DefaultEncryptorManager; import org.apache.syncope.core.spring.security.DummyImplementationLookup; +import org.apache.syncope.core.spring.security.SecurityProperties; import org.jenkinsci.plugins.scriptsecurity.sandbox.blacklists.Blacklist; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -37,14 +38,18 @@ @Configuration(proxyBeanMethods = false) public class SpringTestConfiguration { + public static final String AES_SECRET_KEY = "1abcdefghilmnopq"; + @Bean public ApplicationContextProvider applicationContextProvider() { return new ApplicationContextProvider(); } @Bean public EncryptorManager encryptorManager() { - return new DefaultEncryptorManager(); + SecurityProperties securityProperties = new SecurityProperties(); + securityProperties.setAesSecretKey(AES_SECRET_KEY); + return new DefaultEncryptorManager(securityProperties); } @Primary
core/starter/src/main/resources/core.properties+8 −1 modified@@ -91,7 +91,14 @@ security.jwtIssuer=ApacheSyncope security.jwsAlgorithm=HS512 security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +# Key length drives AES algorithm variant selection: +# +# * 16 chars => AES-128 +# * 24 chars => AES-192 +# * 32 chars => AES-256 +# +# Shorter keys will be padded to the nearest longer option available; keys > 32 will be trucated +security.aesSecretKey=${secretKey} # default for LDAP / RFC2307 SSHA security.digester.saltIterations=1
core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java+2 −2 modified@@ -137,8 +137,8 @@ protected Pair<Boolean, Boolean> enforcePolicies( matching = pwdHistory.subList(policy.getHistoryLength() >= pwdHistory.size() ? 0 : pwdHistory.size() - policy.getHistoryLength(), pwdHistory.size()).stream(). - map(old -> encryptorManager.getInstance().verify( - clearPassword, user.getCipherAlgorithm(), old)). + map(old -> encryptorManager.getInstance(). + verify(clearPassword, user.getCipherAlgorithm(), old)). reduce(matching, (accumulator, item) -> accumulator | item); } if (matching) {
fit/core-reference/src/main/resources/core-embedded.properties+1 −1 modified@@ -30,7 +30,7 @@ spring.devtools.restart.enabled=false security.adminUser=${adminUser} security.anonymousUser=${anonymousUser} security.jwsKey=${jwsKey} -security.secretKey=${secretKey} +security.aesSecretKey=NyefOIpekEJVBASbbETMbcns11HouPNn persistence.domain[0].key=Master persistence.domain[0].jdbcDriver=org.postgresql.Driver
fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java+7 −1 modified@@ -177,6 +177,7 @@ import org.apache.syncope.common.rest.api.service.wa.WebAuthnRegistrationService; import org.apache.syncope.core.persistence.api.EncryptorManager; import org.apache.syncope.core.spring.security.DefaultEncryptorManager; +import org.apache.syncope.core.spring.security.SecurityProperties; import org.apache.syncope.fit.AbstractITCase.KeymasterInitializer; import org.apache.syncope.fit.core.AbstractTaskITCase; import org.apache.syncope.fit.core.CoreITContext; @@ -1154,6 +1155,11 @@ protected static void verifyMail( @Autowired protected DataSource testDataSource; - protected final EncryptorManager encryptorManager = new DefaultEncryptorManager(); + protected final EncryptorManager encryptorManager; + protected AbstractITCase() { + SecurityProperties securityProperties = new SecurityProperties(); + securityProperties.setAesSecretKey(StringUtils.EMPTY); + encryptorManager = new DefaultEncryptorManager(securityProperties); + } }
fit/core-reference/src/test/java/org/apache/syncope/fit/core/KeymasterITCase.java+2 −2 modified@@ -337,8 +337,8 @@ public void domainUpdateAdminPassword() throws Exception { try { // 1. change admin pwd for domain Two domainOps.changeAdminPassword(two.getKey(), - encryptorManager.getInstance().encode("password3", CipherAlgorithm.AES), - CipherAlgorithm.AES); + encryptorManager.getInstance().encode("password3", CipherAlgorithm.BCRYPT), + CipherAlgorithm.BCRYPT); // 2. attempt to access with old pwd -> fail try {
pom.xml+1 −1 modified@@ -496,7 +496,7 @@ under the License. <testcontainers.version>2.0.2</testcontainers.version> - <zonky.embedded-postgres.version>2.1.1</zonky.embedded-postgres.version> + <zonky.embedded-postgres.version>2.2.0</zonky.embedded-postgres.version> <zonky.embedded-postgres-binaries.version>17.6.0</zonky.embedded-postgres-binaries.version> <testds.rootDn>o=isp</testds.rootDn>
src/main/asciidoc/reference-guide/configuration/configurationparameters.adoc+9 −6 modified@@ -24,13 +24,16 @@ Most run-time configuration options are available as parameters and can be tuned * `password.cipher.algorithm` - which cipher algorithm shall be used for encrypting password values; supported algorithms include `SHA-1`, `SHA-256`, `SHA-512`, `AES`, `S-MD5`, `S-SHA-1`, `S-SHA-256`, `S-SHA-512` and `BCRYPT`; salting options are available in the `core.properties` file; +* `security.aesSecretKey` - used for AES-based encryption / decryption: besides password values, this is also used +whenever reversible encryption is needed, throughout the whole system; [WARNING] -The value of the `security.secretKey` property in the `core.properties` file is used for AES-based encryption / decryption. -Besides password values, this is also used whenever reversible encryption is needed, throughout the whole system. + -When the `secretKey` value has length less than 16, it is right-padded by random characters during startup, to reach -such mininum value. + -It is *strongly* recommended to provide a value long at least 16 characters, in order to avoid unexpected behaviors -at runtime, expecially with high-availability. +The actual length of the `security.aesSecretKey` value is used to drive the AES algorithm variant selection: +16 characters implies `AES-128`, 24 selects `AES-192` and 32 configures `AES-256`. + +When the `security.aesSecretKey` value has length less than 16, between 17 and 23 or between 25 and 31, it is +right-padded by random characters during startup, to reach the nearest option. If the specified value is instead longer +than 32 characters, it is truncated to 32. + +It is *strongly* recommended to provide a value long exactly 16, 24 or 32 characters, in order to avoid unexpected +behaviors at runtime, expecially with high-availability. * `jwt.lifetime.minutes` - validity of https://en.wikipedia.org/wiki/JSON_Web_Token[JSON Web Token^] values used for <<rest-authentication-and-authorization,authentication>> (in minutes); * `notificationjob.cronExpression` -
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-jqg8-m35q-jh7jghsaADVISORY
- lists.apache.org/thread/fjh0tb0d1xkbphc5ogdsc348ppz88ctsghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-65998ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/11/24/1ghsaWEB
- github.com/apache/syncope/commit/297498ebfc86e4996f5e3e4ef7b7f8b1cd82004bghsaWEB
- github.com/apache/syncope/commit/9d706af25d2e60327b8b5b63186f9da51ed79a1dghsaWEB
News mentions
0No linked articles in our index yet.