Skip to content

Commit 2894a9e

Browse files
committed
[SYNCOPE-1996] Security production mode
1 parent 5f7cee2 commit 2894a9e

14 files changed

Lines changed: 167 additions & 59 deletions

File tree

‎core/idrepo/logic/src/test/java/org/apache/syncope/core/logic/UserSelfLogicPasswordResetTest.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void setUp() {
6666
ConfParamOps confParamOps = mock(ConfParamOps.class);
6767
userDAO = mock(UserDAO.class);
6868
securityProperties = new SecurityProperties();
69+
securityProperties.setProductionMode(false);
6970

7071
when(confParamOps.get(any(), eq(StandardConfParams.PASSWORD_RESET_ALLOWED), eq(false), eq(boolean.class))).
7172
thenReturn(true);

‎core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/PersistenceTestContext.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.syncope.core.persistence.jpa.spring.DomainRoutingEntityManagerFactory;
3535
import org.apache.syncope.core.provisioning.api.ConnectorManager;
3636
import org.apache.syncope.core.provisioning.api.ImplementationLookup;
37+
import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
3738
import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
3839
import org.apache.syncope.core.spring.security.DefaultPasswordGenerator;
3940
import org.apache.syncope.core.spring.security.PasswordGenerator;
@@ -120,7 +121,8 @@ public ConnectorManager connectorManager() {
120121
public EncryptorManager encryptorManager() {
121122
SecurityProperties securityProperties = new SecurityProperties();
122123
securityProperties.setAesSecretKey(StringUtils.EMPTY);
123-
return new DefaultEncryptorManager(securityProperties);
124+
securityProperties.setProductionMode(false);
125+
return new DefaultEncryptorManager(new DefaultCredentialChecker("", "", "", "", false), securityProperties);
124126
}
125127

126128
@Bean

‎core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/PersistenceTestContext.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.syncope.core.persistence.neo4j.spring.DomainRoutingDriver;
3333
import org.apache.syncope.core.provisioning.api.ConnectorManager;
3434
import org.apache.syncope.core.provisioning.api.ImplementationLookup;
35+
import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
3536
import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
3637
import org.apache.syncope.core.spring.security.DefaultPasswordGenerator;
3738
import org.apache.syncope.core.spring.security.PasswordGenerator;
@@ -110,7 +111,8 @@ public ConnectorManager connectorManager() {
110111
public EncryptorManager encryptorManager() {
111112
SecurityProperties securityProperties = new SecurityProperties();
112113
securityProperties.setAesSecretKey(StringUtils.EMPTY);
113-
return new DefaultEncryptorManager(securityProperties);
114+
securityProperties.setProductionMode(false);
115+
return new DefaultEncryptorManager(new DefaultCredentialChecker("", "", "", "", false), securityProperties);
114116
}
115117

116118
@Bean

‎core/provisioning-java/src/test/resources/core-test.properties‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
security.productionMode=false
1819
security.adminUser=${adminUser}
1920
security.anonymousUser=${anonymousUser}
2021
security.jwsKey=${jwsKey}

‎core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultCredentialChecker.java‎

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,43 +28,88 @@ public class DefaultCredentialChecker {
2828

2929
private static final Logger LOG = LoggerFactory.getLogger(DefaultCredentialChecker.class);
3030

31+
private static final String DEFAULT_AES_KEY_ERROR_MESSAGE =
32+
"The default AES key property is being used. "
33+
+ "This must be changed to avoid a security breach!";
34+
35+
private static final String DEFAULT_AES_KEY = "1abcdefghilmnopqrstuvz2!";
36+
37+
private static final String DEFAULT_JWS_KEY_ERROR_MESSAGE =
38+
"The default JWKS key property is being used. "
39+
+ "This must be changed to avoid a security breach!";
40+
3141
private static final String DEFAULT_JWS_KEY = "ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f";
3242

43+
private static final String DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE =
44+
"The default adminPassword property is being used. "
45+
+ "This must be changed to avoid a security breach!";
46+
3347
private static final String DEFAULT_ADMIN_PASSWORD =
34-
"DE088591C00CC98B36F5ADAAF7DA2B004CF7F2FE7BBB45B766B6409876E2F3DB13C7905C6AA59464";
48+
"DE088591C00CC98B36F5ADAAF7DA2B004CF7F2FE7BBB45B766B6409876E2F3DB13C7905C6AA59464";
49+
50+
private static final String DEFAULT_ANON_KEY_ERROR_MESSAGE =
51+
"The default anonymousKey property is being used. "
52+
+ "This must be changed to avoid a security breach!";
3553

3654
private static final String DEFAULT_ANON_KEY = "anonymousKey";
3755

38-
private final boolean defaultAdminPasswordInUse;
56+
private final boolean defaultAesKeyInUse;
3957

4058
private final boolean defaultJwsKeyInUse;
4159

60+
private final boolean defaultAdminPasswordInUse;
61+
4262
private final boolean defaultAnonymousKeyInUse;
4363

44-
public DefaultCredentialChecker(final String jwsKey, final String adminPassword, final String anonymousKey) {
64+
private final boolean productionMode;
65+
66+
public DefaultCredentialChecker(
67+
final String aesKey,
68+
final String jwsKey,
69+
final String adminPassword,
70+
final String anonymousKey,
71+
final boolean productionMode) {
72+
73+
defaultAesKeyInUse = DEFAULT_AES_KEY.equals(aesKey);
4574
defaultJwsKeyInUse = DEFAULT_JWS_KEY.equals(jwsKey);
4675
defaultAdminPasswordInUse = DEFAULT_ADMIN_PASSWORD.equals(adminPassword);
4776
defaultAnonymousKeyInUse = DEFAULT_ANON_KEY.equals(anonymousKey);
77+
this.productionMode = productionMode;
78+
}
79+
80+
public void checkIsDefaultAESKeyInUse() {
81+
if (defaultAesKeyInUse) {
82+
if (productionMode) {
83+
throw new IllegalStateException(DEFAULT_AES_KEY_ERROR_MESSAGE);
84+
}
85+
LOG.warn(DEFAULT_AES_KEY_ERROR_MESSAGE);
86+
}
4887
}
4988

5089
public void checkIsDefaultJWSKeyInUse() {
5190
if (defaultJwsKeyInUse) {
52-
LOG.warn("The default jwsKey property is being used. "
53-
+ "This must be changed to avoid a security breach!");
91+
if (productionMode) {
92+
throw new IllegalStateException(DEFAULT_JWS_KEY_ERROR_MESSAGE);
93+
}
94+
LOG.warn(DEFAULT_JWS_KEY_ERROR_MESSAGE);
5495
}
5596
}
5697

5798
public void checkIsDefaultAdminPasswordInUse() {
5899
if (defaultAdminPasswordInUse) {
59-
LOG.warn("The default adminPassword property is being used. "
60-
+ "This must be changed to avoid a security breach!");
100+
if (productionMode) {
101+
throw new IllegalStateException(DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE);
102+
}
103+
LOG.warn(DEFAULT_ADMIN_PASSWORD_ERROR_MESSAGE);
61104
}
62105
}
63106

64107
public void checkIsDefaultAnonymousKeyInUse() {
65108
if (defaultAnonymousKeyInUse) {
66-
LOG.warn("The default anonymousKey property is being used. "
67-
+ "This must be changed to avoid a security breach!");
109+
if (productionMode) {
110+
throw new IllegalStateException(DEFAULT_ANON_KEY_ERROR_MESSAGE);
111+
}
112+
LOG.warn(DEFAULT_ANON_KEY_ERROR_MESSAGE);
68113
}
69114
}
70115
}

‎core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptor.java‎

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class DefaultEncryptor implements Encryptor {
5151

5252
protected DefaultEncryptor(
5353
final String aesSecretKey,
54+
final boolean productionMode,
5455
final SecurityProperties.DigesterProperties digesterProperties) {
5556

5657
this.digesterProperties = digesterProperties;
@@ -60,30 +61,34 @@ protected DefaultEncryptor(
6061
if (StringUtils.isNotBlank(aesSecretKey)) {
6162
String actualKey = aesSecretKey;
6263

63-
Integer pad = null;
64-
boolean truncate = false;
65-
if (actualKey.length() < 16) {
66-
pad = 16 - actualKey.length();
67-
} else if (actualKey.length() > 16 && actualKey.length() < 24) {
68-
pad = 24 - actualKey.length();
69-
} else if (actualKey.length() > 24 && actualKey.length() < 32) {
70-
pad = 32 - actualKey.length();
71-
} else if (actualKey.length() > 32) {
72-
truncate = true;
73-
}
64+
if (!productionMode) {
65+
Integer pad = null;
66+
boolean truncate = false;
67+
if (actualKey.length() < 16) {
68+
pad = 16 - actualKey.length();
69+
} else if (actualKey.length() > 16 && actualKey.length() < 24) {
70+
pad = 24 - actualKey.length();
71+
} else if (actualKey.length() > 24 && actualKey.length() < 32) {
72+
pad = 32 - actualKey.length();
73+
} else if (actualKey.length() > 32) {
74+
truncate = true;
75+
}
7476

75-
if (pad != null) {
76-
StringBuilder actualKeyPadding = new StringBuilder(actualKey);
77-
String randomChars = SecureRandomUtils.generateRandomPassword(pad);
77+
if (pad != null) {
78+
StringBuilder actualKeyPadding = new StringBuilder(actualKey);
79+
String randomChars = SecureRandomUtils.generateRandomPassword(pad);
7880

79-
actualKeyPadding.append(randomChars);
80-
actualKey = actualKeyPadding.toString();
81-
LOG.warn("The configured AES secret key is too short (< {}), padding with random chars: {}",
82-
actualKey.length(), actualKey);
83-
}
84-
if (truncate) {
85-
actualKey = actualKey.substring(0, 32);
86-
LOG.warn("The configured AES secret key is too long (> 32), truncating: {}", actualKey);
81+
actualKeyPadding.append(randomChars);
82+
actualKey = actualKeyPadding.toString();
83+
LOG.warn("The configured AES secret key is too short (< {}), padding with random chars",
84+
actualKey.length());
85+
LOG.debug("Using\nsecurity.aesSecretKey={}", actualKey);
86+
}
87+
if (truncate) {
88+
actualKey = actualKey.substring(0, 32);
89+
LOG.warn("The configured AES secret key is too long (> 32), truncating");
90+
LOG.debug("Using\nsecurity.aesSecretKey={}", actualKey);
91+
}
8792
}
8893

8994
try {

‎core/spring/src/main/java/org/apache/syncope/core/spring/security/DefaultEncryptorManager.java‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@
2626

2727
public class DefaultEncryptorManager implements EncryptorManager {
2828

29+
protected final DefaultCredentialChecker credentialChecker;
30+
2931
protected final SecurityProperties securityProperties;
3032

3133
protected final Map<String, DefaultEncryptor> instances = new ConcurrentHashMap<>();
3234

33-
public DefaultEncryptorManager(final SecurityProperties securityProperties) {
35+
public DefaultEncryptorManager(
36+
final DefaultCredentialChecker credentialChecker,
37+
final SecurityProperties securityProperties) {
38+
39+
this.credentialChecker = credentialChecker;
3440
this.securityProperties = securityProperties;
3541
}
3642

@@ -41,7 +47,11 @@ public Encryptor getInstance() {
4147

4248
@Override
4349
public Encryptor getInstance(final String aesSecretKey) {
50+
credentialChecker.checkIsDefaultAESKeyInUse();
51+
4452
String actualKey = StringUtils.isBlank(aesSecretKey) ? securityProperties.getAesSecretKey() : aesSecretKey;
45-
return instances.computeIfAbsent(actualKey, k -> new DefaultEncryptor(k, securityProperties.getDigester()));
53+
return instances.computeIfAbsent(
54+
actualKey,
55+
k -> new DefaultEncryptor(k, securityProperties.isProductionMode(), securityProperties.getDigester()));
4656
}
4757
}

‎core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java‎

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,31 +65,45 @@ public class SecurityContext {
6565

6666
private static final Logger LOG = LoggerFactory.getLogger(SecurityContext.class);
6767

68+
private static String JWS_KEY = null;
69+
6870
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
6971
@Bean
7072
public static GrantedAuthorityDefaults grantedAuthorityDefaults() {
7173
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
7274
}
7375

74-
protected static String jwsKey(final JWSAlgorithm jwsAlgorithm, final SecurityProperties props) {
75-
String jwsKey = Optional.ofNullable(props.getJwsKey()).
76-
orElseThrow(() -> new IllegalArgumentException("No JWS key provided"));
77-
78-
if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
79-
int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256)
80-
? 256 / 8
81-
: jwsAlgorithm.equals(JWSAlgorithm.HS384)
82-
? 384 / 8
83-
: 512 / 8;
84-
if (jwsKey.length() < minLength) {
85-
jwsKey = SecureRandomUtils.generateRandomPassword(minLength);
86-
props.setJwsKey(jwsKey);
87-
LOG.warn("The configured key for {} must be at least {} bits, generating random: {}",
88-
jwsAlgorithm, minLength * 8, jwsKey);
76+
private static String jwsKey(final JWSAlgorithm jwsAlgorithm, final SecurityProperties props) {
77+
synchronized (LOG) {
78+
if (JWS_KEY == null) {
79+
String jwsKey = Optional.ofNullable(props.getJwsKey()).
80+
orElseThrow(() -> new IllegalArgumentException("No JWS key provided"));
81+
82+
if (JWSAlgorithm.Family.HMAC_SHA.contains(jwsAlgorithm)) {
83+
int minLength = jwsAlgorithm.equals(JWSAlgorithm.HS256)
84+
? 256 / 8
85+
: jwsAlgorithm.equals(JWSAlgorithm.HS384)
86+
? 384 / 8
87+
: 512 / 8;
88+
if (jwsKey.length() < minLength) {
89+
if (props.isProductionMode()) {
90+
throw new IllegalArgumentException(
91+
"The configured key for %s must be at least %d characters".
92+
formatted(jwsAlgorithm, minLength));
93+
}
94+
95+
jwsKey = SecureRandomUtils.generateRandomPassword(minLength);
96+
props.setJwsKey(jwsKey);
97+
LOG.warn("The configured key for {} must be at least {} characters, generating random",
98+
jwsAlgorithm, minLength);
99+
LOG.debug("Using\nsecurity.jwsKey={}", jwsKey);
100+
}
101+
}
102+
103+
JWS_KEY = jwsKey;
89104
}
90105
}
91-
92-
return jwsKey;
106+
return JWS_KEY;
93107
}
94108

95109
@Bean
@@ -109,9 +123,11 @@ public DefaultCredentialChecker credentialChecker(
109123
final JWSAlgorithm jwsAlgorithm) {
110124

111125
return new DefaultCredentialChecker(
126+
props.getAesSecretKey(),
112127
jwsKey(jwsAlgorithm, props),
113128
props.getAdminPassword(),
114-
props.getAnonymousKey());
129+
props.getAnonymousKey(),
130+
props.isProductionMode());
115131
}
116132

117133
@ConditionalOnMissingBean
@@ -164,8 +180,11 @@ public ApplicationContextProvider applicationContextProvider() {
164180
}
165181

166182
@Bean
167-
public EncryptorManager encryptorManager(final SecurityProperties securityProperties) {
168-
return new DefaultEncryptorManager(securityProperties);
183+
public EncryptorManager encryptorManager(
184+
final DefaultCredentialChecker credentialChecker,
185+
final SecurityProperties securityProperties) {
186+
187+
return new DefaultEncryptorManager(credentialChecker, securityProperties);
169188
}
170189

171190
@ConditionalOnMissingBean

‎core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityProperties.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public void setUseLenientSaltSizeCheck(final boolean useLenientSaltSizeCheck) {
150150
}
151151
}
152152

153+
private boolean productionMode = true;
154+
153155
private String adminUser;
154156

155157
private String adminPassword;
@@ -182,6 +184,14 @@ public void setUseLenientSaltSizeCheck(final boolean useLenientSaltSizeCheck) {
182184

183185
private final DigesterProperties digester = new DigesterProperties();
184186

187+
public boolean isProductionMode() {
188+
return productionMode;
189+
}
190+
191+
public void setProductionMode(final boolean productionMode) {
192+
this.productionMode = productionMode;
193+
}
194+
185195
public String getAdminUser() {
186196
return adminUser;
187197
}

‎core/spring/src/test/java/org/apache/syncope/core/spring/SpringTestConfiguration.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.syncope.core.persistence.api.ApplicationContextProvider;
2626
import org.apache.syncope.core.persistence.api.EncryptorManager;
2727
import org.apache.syncope.core.provisioning.api.ImplementationLookup;
28+
import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
2829
import org.apache.syncope.core.spring.security.DefaultEncryptorManager;
2930
import org.apache.syncope.core.spring.security.DummyImplementationLookup;
3031
import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -49,7 +50,7 @@ public ApplicationContextProvider applicationContextProvider() {
4950
public EncryptorManager encryptorManager() {
5051
SecurityProperties securityProperties = new SecurityProperties();
5152
securityProperties.setAesSecretKey(AES_SECRET_KEY);
52-
return new DefaultEncryptorManager(securityProperties);
53+
return new DefaultEncryptorManager(new DefaultCredentialChecker("", "", "", "", false), securityProperties);
5354
}
5455

5556
@Primary

0 commit comments

Comments
 (0)