This is an automated email from the ASF dual-hosted git repository.
jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git
The following commit(s) were added to refs/heads/main by this push:
new bac579a ARTEMIS-3573 Support PropertiesLoginModule custom password
codecs
new fd9eb2e This closes #3851
bac579a is described below
commit bac579ac253ef0566adb2316264c8abab0b82272
Author: Domenico Francesco Bruscino <[email protected]>
AuthorDate: Mon Feb 7 21:46:10 2022 +0100
ARTEMIS-3573 Support PropertiesLoginModule custom password codecs
---
.../artemis/utils/DefaultSensitiveStringCodec.java | 12 +++++
...reHashProcessor.java => LazyHashProcessor.java} | 35 +++++++-----
.../artemis/utils/PasswordMaskingUtil.java | 7 +++
.../artemis/utils/SecureHashProcessor.java | 4 +-
.../activemq/artemis/utils/SensitiveDataCodec.java | 8 ++-
.../utils/DefaultSensitiveStringCodecTest.java | 62 ++++++++++++++++------
.../core/security/jaas/PropertiesLoginModule.java | 18 ++++++-
docs/user-manual/en/masking-passwords.md | 45 ++++++++++------
docs/user-manual/en/security.md | 4 ++
.../tests/integration/security/SecurityTest.java | 34 ++++++++++++
.../src/test/resources/login.config | 7 +++
.../src/test/resources/roles.properties | 1 +
.../src/test/resources/users.properties | 1 +
13 files changed, 184 insertions(+), 54 deletions(-)
diff --git
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
index 83c2ba7..6f07a7b 100644
---
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
+++
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodec.java
@@ -28,6 +28,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Properties;
import org.jboss.logging.Logger;
@@ -105,6 +106,7 @@ public class DefaultSensitiveStringCodec implements
SensitiveDataCodec<String> {
System.out.println("Encoded password (without quotes): \"" + encode +
"\"");
}
+ @Override
public boolean verify(char[] inputValue, String storedValue) {
return algorithm.verify(inputValue, storedValue);
}
@@ -188,6 +190,16 @@ public class DefaultSensitiveStringCodec implements
SensitiveDataCodec<String> {
BigInteger n = new BigInteger(encoding);
return n.toString(16);
}
+
+ @Override
+ public boolean verify(char[] inputValue, String storedValue) {
+ try {
+ return Objects.equals(storedValue,
encode(String.valueOf(inputValue)));
+ } catch (Exception e) {
+ logger.debug("Exception on verifying: " + e);
+ return false;
+ }
+ }
}
private class PBKDF2Algorithm extends CodecAlgorithm {
diff --git
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/LazyHashProcessor.java
similarity index 55%
copy from
artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
copy to
artemis-commons/src/main/java/org/apache/activemq/artemis/utils/LazyHashProcessor.java
index a17cfd4..cfde12d 100644
---
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
+++
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/LazyHashProcessor.java
@@ -1,38 +1,45 @@
-/*
+/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.activemq.artemis.utils;
-public class SecureHashProcessor implements HashProcessor {
+import java.util.function.Supplier;
- private DefaultSensitiveStringCodec codec;
+public abstract class LazyHashProcessor implements HashProcessor {
- public SecureHashProcessor(DefaultSensitiveStringCodec codec) {
- this.codec = codec;
- }
+ private Supplier<HashProcessor> hashProcessorSupplier =
Suppliers.memoize(() -> {
+ try {
+ return createHashProcessor();
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
@Override
public String hash(String plainText) throws Exception {
- return PasswordMaskingUtil.wrap(codec.encode(plainText));
+ return hashProcessorSupplier.get().hash(plainText);
}
@Override
- //storedValue must take form of ENC(...)
- public boolean compare(char[] inputValue, String storedValue) {
- String storedHash = storedValue.substring(4, storedValue.length() - 1);
- return codec.verify(inputValue, storedHash);
+ public boolean compare(char[] inputValue, String storedHash) {
+ return hashProcessorSupplier.get().compare(inputValue, storedHash);
}
+
+ protected abstract HashProcessor createHashProcessor() throws Exception;
}
diff --git
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java
index 4c64a45..2287e53 100644
---
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java
+++
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/PasswordMaskingUtil.java
@@ -122,10 +122,17 @@ public final class PasswordMaskingUtil {
//stored password takes 2 forms, ENC() or plain text
public static HashProcessor getHashProcessor(String storedPassword) {
+ return getHashProcessor(storedPassword, null);
+ }
+
+ public static HashProcessor getHashProcessor(String storedPassword,
HashProcessor secureHashProcessor) {
if (!isEncoded(storedPassword)) {
return LazyPlainTextProcessorHolder.INSTANCE;
}
+ if (secureHashProcessor != null) {
+ return secureHashProcessor;
+ }
final Exception secureProcessorException =
LazySecureProcessorHolder.EXCEPTION;
if (secureProcessorException != null) {
//reuse old descriptions/messages of the exception but refill the
stack trace
diff --git
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
index a17cfd4..544b0b0 100644
---
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
+++
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SecureHashProcessor.java
@@ -18,9 +18,9 @@ package org.apache.activemq.artemis.utils;
public class SecureHashProcessor implements HashProcessor {
- private DefaultSensitiveStringCodec codec;
+ private SensitiveDataCodec<String> codec;
- public SecureHashProcessor(DefaultSensitiveStringCodec codec) {
+ public SecureHashProcessor(SensitiveDataCodec<String> codec) {
this.codec = codec;
}
diff --git
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java
index 5f3c08a..015af65 100644
---
a/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java
+++
b/artemis-commons/src/main/java/org/apache/activemq/artemis/utils/SensitiveDataCodec.java
@@ -27,10 +27,14 @@ import java.util.Map;
*/
public interface SensitiveDataCodec<T> {
- T decode(Object mask) throws Exception;
+ T decode(Object encodedValue) throws Exception;
- T encode(Object secret) throws Exception;
+ T encode(Object value) throws Exception;
default void init(Map<String, String> params) throws Exception {
}
+
+ default boolean verify(char[] value, T encodedValue) {
+ return false;
+ }
}
diff --git
a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
index 9a97454..c670345 100644
---
a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
+++
b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/DefaultSensitiveStringCodecTest.java
@@ -39,20 +39,33 @@ public class DefaultSensitiveStringCodecTest {
@Test
public void testOnewayAlgorithm() throws Exception {
- DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
- Map<String, String> params = new HashMap<>();
- params.put(DefaultSensitiveStringCodec.ALGORITHM,
DefaultSensitiveStringCodec.ONE_WAY);
- codec.init(params);
+ testAlgorithm(DefaultSensitiveStringCodec.ONE_WAY);
+ }
+
+ @Test
+ public void testTwowayAlgorithm() throws Exception {
+ testAlgorithm(DefaultSensitiveStringCodec.TWO_WAY);
+ }
+
+ private void testAlgorithm(String algorithm) throws Exception {
+ DefaultSensitiveStringCodec codec =
getDefaultSensitiveStringCodec(algorithm);
String plainText = "some_password";
String maskedText = codec.encode(plainText);
log.debug("encoded value: " + maskedText);
- //one way can't decode
- try {
- codec.decode(maskedText);
- fail("one way algorithm can't decode");
- } catch (IllegalArgumentException expected) {
+ if (algorithm.equals(DefaultSensitiveStringCodec.ONE_WAY)) {
+ //one way can't decode
+ try {
+ codec.decode(maskedText);
+ fail("one way algorithm can't decode");
+ } catch (IllegalArgumentException expected) {
+ }
+ } else {
+ String decoded = codec.decode(maskedText);
+ log.debug("encoded value: " + maskedText);
+
+ assertEquals("decoded result not match: " + decoded, decoded,
plainText);
}
assertTrue(codec.verify(plainText.toCharArray(), maskedText));
@@ -62,19 +75,34 @@ public class DefaultSensitiveStringCodecTest {
}
@Test
- public void testTwowayAlgorithm() throws Exception {
- DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
- Map<String, String> params = new HashMap<>();
- params.put(DefaultSensitiveStringCodec.ALGORITHM,
DefaultSensitiveStringCodec.TWO_WAY);
- codec.init(params);
+ public void testCompareWithOnewayAlgorithm() throws Exception {
+ testCompareWithAlgorithm(DefaultSensitiveStringCodec.ONE_WAY);
+ }
+
+ @Test
+ public void testCompareWithTwowayAlgorithm() throws Exception {
+ testCompareWithAlgorithm(DefaultSensitiveStringCodec.TWO_WAY);
+ }
+
+ private void testCompareWithAlgorithm(String algorithm) throws Exception {
+ DefaultSensitiveStringCodec codec =
getDefaultSensitiveStringCodec(algorithm);
String plainText = "some_password";
String maskedText = codec.encode(plainText);
log.debug("encoded value: " + maskedText);
- String decoded = codec.decode(maskedText);
- log.debug("encoded value: " + maskedText);
+ assertTrue(codec.verify(plainText.toCharArray(), maskedText));
+
+ String otherPassword = "some_other_password";
+ assertFalse(codec.verify(otherPassword.toCharArray(), maskedText));
+ }
+
+ private DefaultSensitiveStringCodec getDefaultSensitiveStringCodec(String
algorithm) throws Exception {
+ DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec();
+ Map<String, String> params = new HashMap<>();
+ params.put(DefaultSensitiveStringCodec.ALGORITHM, algorithm);
+ codec.init(params);
- assertEquals("decoded result not match: " + decoded, decoded, plainText);
+ return codec;
}
}
diff --git
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java
index e021b5b..79af7f3 100644
---
a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java
+++
b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PropertiesLoginModule.java
@@ -32,7 +32,9 @@ import java.util.Properties;
import java.util.Set;
import org.apache.activemq.artemis.utils.HashProcessor;
+import org.apache.activemq.artemis.utils.LazyHashProcessor;
import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+import org.apache.activemq.artemis.utils.SecureHashProcessor;
import org.jboss.logging.Logger;
public class PropertiesLoginModule extends PropertiesLoader implements
AuditLoginModule {
@@ -41,6 +43,7 @@ public class PropertiesLoginModule extends PropertiesLoader
implements AuditLogi
public static final String USER_FILE_PROP_NAME =
"org.apache.activemq.jaas.properties.user";
public static final String ROLE_FILE_PROP_NAME =
"org.apache.activemq.jaas.properties.role";
+ public static final String PASSWORD_CODEC_PROP_NAME =
"org.apache.activemq.jaas.properties.password.codec";
private Subject subject;
private CallbackHandler callbackHandler;
@@ -64,10 +67,21 @@ public class PropertiesLoginModule extends PropertiesLoader
implements AuditLogi
init(options);
users = load(USER_FILE_PROP_NAME, "user", options).getProps();
roles = load(ROLE_FILE_PROP_NAME, "role",
options).invertedPropertiesValuesMap();
+
+ String passwordCodec = (String)options.get(PASSWORD_CODEC_PROP_NAME);
+ if (passwordCodec != null) {
+ hashProcessor = new LazyHashProcessor() {
+ @Override
+ protected HashProcessor createHashProcessor() throws Exception {
+ return new
SecureHashProcessor(PasswordMaskingUtil.getCodec(passwordCodec));
+ }
+ };
+ }
}
@Override
public boolean login() throws LoginException {
+ HashProcessor userHashProcessor;
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username: ");
@@ -94,12 +108,12 @@ public class PropertiesLoginModule extends
PropertiesLoader implements AuditLogi
}
try {
- hashProcessor = PasswordMaskingUtil.getHashProcessor(password);
+ userHashProcessor = PasswordMaskingUtil.getHashProcessor(password,
hashProcessor);
} catch (Exception e) {
throw new FailedLoginException("Failed to get hash processor");
}
- if (!hashProcessor.compare(tmpPassword, password)) {
+ if (!userHashProcessor.compare(tmpPassword, password)) {
throw new FailedLoginException("Password does not match for user: " +
user);
}
loginSucceeded = true;
diff --git a/docs/user-manual/en/masking-passwords.md
b/docs/user-manual/en/masking-passwords.md
index d104823..24400ca 100644
--- a/docs/user-manual/en/masking-passwords.md
+++ b/docs/user-manual/en/masking-passwords.md
@@ -259,7 +259,26 @@ codec other than the default one. For example
With this configuration, both passwords in ra.xml and all of its MDBs will have
to be in masked form.
-### login.config
+### PropertiesLoginModule
+Artemis supports Properties login module to be configured in JAAS
configuration file
+(default name is `login.config`). By default, the passwords of the users are
in plain text
+or masked with the [the default codec](#the-default-codec).
+
+To use a custom codec class, set the
`org.apache.activemq.jaas.properties.password.codec` property to the class name
+e.g. to use the `com.example.MySensitiveDataCodecImpl` codec class:
+
+```
+PropertiesLoginWithPasswordCodec {
+ org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule
required
+ debug=true
+ org.apache.activemq.jaas.properties.user="users.properties"
+ org.apache.activemq.jaas.properties.role="roles.properties"
+
org.apache.activemq.jaas.properties.password.codec="com.example.MySensitiveDataCodecImpl";
+};
+```
+
+
+### LDAPLoginModule
Artemis supports LDAP login modules to be configured in JAAS configuration file
(default name is `login.config`). When connecting to an LDAP server usually you
@@ -429,22 +448,8 @@ using the new defined codec.
To use a different codec than the built-in one, you either pick one from
existing libraries or you implement it yourself. All codecs must implement
-the `org.apache.activemq.artemis.utils.SensitiveDataCodec<T>` interface:
-
-```java
-public interface SensitiveDataCodec<T> {
-
- T decode(Object mask) throws Exception;
-
- T encode(Object secret) throws Exception;
-
- default void init(Map<String, String> params) throws Exception {
- };
-}
-```
-
-This is a generic type interface but normally for a password you just need
-String type. So a new codec would be defined like
+the `org.apache.activemq.artemis.utils.SensitiveDataCodec<String>` interface.
+So a new codec would be defined like
```java
public class MyCodec implements SensitiveDataCodec<String> {
@@ -464,6 +469,12 @@ public class MyCodec implements SensitiveDataCodec<String>
{
public void init(Map<String, String> params) {
// Initialization done here. It is called right after the codec has been
created.
}
+
+ @Override
+ public boolean verify(char[] value, String encodedValue) {
+ // Return true if the value matches the encodedValue.
+ return checkValueMatchesEncoding(value, encodedValue);
+ }
}
```
diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md
index 8c8e881..f5a3ea5 100644
--- a/docs/user-manual/en/security.md
+++ b/docs/user-manual/en/security.md
@@ -567,6 +567,10 @@ integration with LDAP is preferable. It is implemented by
- `org.apache.activemq.jaas.properties.role` - the path to the file which
contains user and role properties
+- `org.apache.activemq.jaas.properties.password.codec` - the fully qualified
+ class name of the password codec to use. See the [password
masking](masking-passwords.md)
+ documentation for more details on how this works.
+
- `reload` - boolean flag; whether or not to reload the properties files when a
modification occurs; default is `false`
diff --git
a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java
b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java
index 45b1082..ba33949 100644
---
a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java
+++
b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/security/SecurityTest.java
@@ -71,6 +71,7 @@ import
org.apache.activemq.artemis.spi.core.security.ActiveMQSecurityManager4;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.CreateMessage;
import org.apache.activemq.artemis.utils.CompositeAddress;
+import org.apache.activemq.artemis.utils.SensitiveDataCodec;
import org.apache.activemq.artemis.utils.Wait;
import org.apache.activemq.command.ActiveMQQueue;
import org.junit.Assert;
@@ -130,6 +131,22 @@ public class SecurityTest extends ActiveMQTestBase {
}
@Test
+ public void testJAASSecurityManagerAuthenticationWithPasswordCodec() throws
Exception {
+ ActiveMQJAASSecurityManager securityManager = new
ActiveMQJAASSecurityManager("PropertiesLoginWithPasswordCodec");
+ ActiveMQServer server =
addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true),
ManagementFactory.getPlatformMBeanServer(), securityManager, false));
+ server.start();
+ ClientSessionFactory cf = createSessionFactory(locator);
+
+ try {
+ ClientSession session = cf.createSession("test","password", false,
true, true, false, 0);
+ session.close();
+ } catch (ActiveMQException e) {
+ e.printStackTrace();
+ Assert.fail("should not throw exception");
+ }
+ }
+
+ @Test
public void testJAASSecurityManagerAuthenticationWithValidateUser() throws
Exception {
ActiveMQJAASSecurityManager securityManager = new
ActiveMQJAASSecurityManager("PropertiesLogin");
ActiveMQServer server =
addServer(ActiveMQServers.newActiveMQServer(createDefaultInVMConfig().setSecurityEnabled(true),
ManagementFactory.getPlatformMBeanServer(), securityManager, false));
@@ -2564,4 +2581,21 @@ public class SecurityTest extends ActiveMQTestBase {
fail("Invalid Exception type:" + e.getType());
}
}
+
+ public static class DummySensitiveDataCodec implements
SensitiveDataCodec<String> {
+ @Override
+ public String decode(Object encodedValue) throws Exception {
+ throw new IllegalStateException("Decoding not supported");
+ }
+
+ @Override
+ public String encode(Object value) throws Exception {
+ return new StringBuffer((String)value).reverse().toString();
+ }
+
+ @Override
+ public boolean verify(char[] value, String encodedValue) {
+ return encodedValue.equals(new
StringBuffer(String.valueOf(value)).reverse().toString());
+ }
+ }
}
diff --git a/tests/integration-tests/src/test/resources/login.config
b/tests/integration-tests/src/test/resources/login.config
index b1af825..f1fe8df 100644
--- a/tests/integration-tests/src/test/resources/login.config
+++ b/tests/integration-tests/src/test/resources/login.config
@@ -21,6 +21,13 @@ PropertiesLogin {
org.apache.activemq.jaas.properties.role="roles.properties";
};
+PropertiesLoginWithPasswordCodec {
+ org.apache.activemq.artemis.spi.core.security.jaas.PropertiesLoginModule
required
+ debug=true
+ org.apache.activemq.jaas.properties.user="users.properties"
+ org.apache.activemq.jaas.properties.role="roles.properties"
+
org.apache.activemq.jaas.properties.password.codec="org.apache.activemq.artemis.tests.integration.security.SecurityTest$DummySensitiveDataCodec";
+};
LDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required
diff --git a/tests/integration-tests/src/test/resources/roles.properties
b/tests/integration-tests/src/test/resources/roles.properties
index 31b0bd2..41ca59d 100644
--- a/tests/integration-tests/src/test/resources/roles.properties
+++ b/tests/integration-tests/src/test/resources/roles.properties
@@ -17,6 +17,7 @@
programmers=first
accounting=second
+test=test
employees=first,second
a=a
b=b
diff --git a/tests/integration-tests/src/test/resources/users.properties
b/tests/integration-tests/src/test/resources/users.properties
index d4c65e9..a275fad 100644
--- a/tests/integration-tests/src/test/resources/users.properties
+++ b/tests/integration-tests/src/test/resources/users.properties
@@ -17,6 +17,7 @@
first=secret
second=password
+test=ENC(drowssap)
a=a
b=b
x=x