VYPR
High severityNVD Advisory· Published Nov 24, 2025· Updated Nov 24, 2025

Apache Syncope: Default AES key used for internal password encryption

CVE-2025-65998

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.

PackageAffected versionsPatched versions
org.apache.syncope:syncope-coreMaven
>= 4.0.0, < 4.0.34.0.3
org.apache.syncope:syncope-coreMaven
< 3.0.153.0.15

Affected products

2
  • Apache/Syncopellm-fuzzy
    Range: <3.0.15, <4.0.3
  • Apache Software Foundation/Apache Syncopev5
    Range: 2.1

Patches

2
9d706af25d2e

[SYNCOPE-1932] Improve AES management (#1243)

https://github.com/apache/syncopeFrancesco ChicchiriccòNov 19, 2025via ghsa
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)

https://github.com/apache/syncopeFrancesco ChicchiriccòNov 19, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.