This commit is contained in:
Justin Bertram 2022-02-07 20:31:30 -06:00
commit fd9eb2ec9f
No known key found for this signature in database
GPG Key ID: F41830B875BB8633
13 changed files with 208 additions and 40 deletions

View File

@ -28,6 +28,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Properties; import java.util.Properties;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -105,6 +106,7 @@ public class DefaultSensitiveStringCodec implements SensitiveDataCodec<String> {
System.out.println("Encoded password (without quotes): \"" + encode + "\""); System.out.println("Encoded password (without quotes): \"" + encode + "\"");
} }
@Override
public boolean verify(char[] inputValue, String storedValue) { public boolean verify(char[] inputValue, String storedValue) {
return algorithm.verify(inputValue, storedValue); return algorithm.verify(inputValue, storedValue);
} }
@ -188,6 +190,16 @@ public class DefaultSensitiveStringCodec implements SensitiveDataCodec<String> {
BigInteger n = new BigInteger(encoding); BigInteger n = new BigInteger(encoding);
return n.toString(16); 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 { private class PBKDF2Algorithm extends CodecAlgorithm {

View File

@ -0,0 +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
* <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;
import java.util.function.Supplier;
public abstract class LazyHashProcessor implements HashProcessor {
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 hashProcessorSupplier.get().hash(plainText);
}
@Override
public boolean compare(char[] inputValue, String storedHash) {
return hashProcessorSupplier.get().compare(inputValue, storedHash);
}
protected abstract HashProcessor createHashProcessor() throws Exception;
}

View File

@ -122,10 +122,17 @@ public final class PasswordMaskingUtil {
//stored password takes 2 forms, ENC() or plain text //stored password takes 2 forms, ENC() or plain text
public static HashProcessor getHashProcessor(String storedPassword) { public static HashProcessor getHashProcessor(String storedPassword) {
return getHashProcessor(storedPassword, null);
}
public static HashProcessor getHashProcessor(String storedPassword, HashProcessor secureHashProcessor) {
if (!isEncoded(storedPassword)) { if (!isEncoded(storedPassword)) {
return LazyPlainTextProcessorHolder.INSTANCE; return LazyPlainTextProcessorHolder.INSTANCE;
} }
if (secureHashProcessor != null) {
return secureHashProcessor;
}
final Exception secureProcessorException = LazySecureProcessorHolder.EXCEPTION; final Exception secureProcessorException = LazySecureProcessorHolder.EXCEPTION;
if (secureProcessorException != null) { if (secureProcessorException != null) {
//reuse old descriptions/messages of the exception but refill the stack trace //reuse old descriptions/messages of the exception but refill the stack trace

View File

@ -18,9 +18,9 @@ package org.apache.activemq.artemis.utils;
public class SecureHashProcessor implements HashProcessor { public class SecureHashProcessor implements HashProcessor {
private DefaultSensitiveStringCodec codec; private SensitiveDataCodec<String> codec;
public SecureHashProcessor(DefaultSensitiveStringCodec codec) { public SecureHashProcessor(SensitiveDataCodec<String> codec) {
this.codec = codec; this.codec = codec;
} }

View File

@ -27,10 +27,14 @@ import java.util.Map;
*/ */
public interface SensitiveDataCodec<T> { 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 void init(Map<String, String> params) throws Exception {
} }
default boolean verify(char[] value, T encodedValue) {
return false;
}
} }

View File

@ -39,20 +39,33 @@ public class DefaultSensitiveStringCodecTest {
@Test @Test
public void testOnewayAlgorithm() throws Exception { public void testOnewayAlgorithm() throws Exception {
DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); testAlgorithm(DefaultSensitiveStringCodec.ONE_WAY);
Map<String, String> params = new HashMap<>(); }
params.put(DefaultSensitiveStringCodec.ALGORITHM, DefaultSensitiveStringCodec.ONE_WAY);
codec.init(params); @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 plainText = "some_password";
String maskedText = codec.encode(plainText); String maskedText = codec.encode(plainText);
log.debug("encoded value: " + maskedText); log.debug("encoded value: " + maskedText);
//one way can't decode if (algorithm.equals(DefaultSensitiveStringCodec.ONE_WAY)) {
try { //one way can't decode
codec.decode(maskedText); try {
fail("one way algorithm can't decode"); codec.decode(maskedText);
} catch (IllegalArgumentException expected) { 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)); assertTrue(codec.verify(plainText.toCharArray(), maskedText));
@ -62,19 +75,34 @@ public class DefaultSensitiveStringCodecTest {
} }
@Test @Test
public void testTwowayAlgorithm() throws Exception { public void testCompareWithOnewayAlgorithm() throws Exception {
DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); testCompareWithAlgorithm(DefaultSensitiveStringCodec.ONE_WAY);
Map<String, String> params = new HashMap<>(); }
params.put(DefaultSensitiveStringCodec.ALGORITHM, DefaultSensitiveStringCodec.TWO_WAY);
codec.init(params); @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 plainText = "some_password";
String maskedText = codec.encode(plainText); String maskedText = codec.encode(plainText);
log.debug("encoded value: " + maskedText); log.debug("encoded value: " + maskedText);
String decoded = codec.decode(maskedText); assertTrue(codec.verify(plainText.toCharArray(), maskedText));
log.debug("encoded value: " + maskedText);
assertEquals("decoded result not match: " + decoded, decoded, plainText); 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);
return codec;
} }
} }

View File

@ -32,7 +32,9 @@ import java.util.Properties;
import java.util.Set; import java.util.Set;
import org.apache.activemq.artemis.utils.HashProcessor; 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.PasswordMaskingUtil;
import org.apache.activemq.artemis.utils.SecureHashProcessor;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
public class PropertiesLoginModule extends PropertiesLoader implements AuditLoginModule { 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 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 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 Subject subject;
private CallbackHandler callbackHandler; private CallbackHandler callbackHandler;
@ -64,10 +67,21 @@ public class PropertiesLoginModule extends PropertiesLoader implements AuditLogi
init(options); init(options);
users = load(USER_FILE_PROP_NAME, "user", options).getProps(); users = load(USER_FILE_PROP_NAME, "user", options).getProps();
roles = load(ROLE_FILE_PROP_NAME, "role", options).invertedPropertiesValuesMap(); 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 @Override
public boolean login() throws LoginException { public boolean login() throws LoginException {
HashProcessor userHashProcessor;
Callback[] callbacks = new Callback[2]; Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("Username: "); callbacks[0] = new NameCallback("Username: ");
@ -94,12 +108,12 @@ public class PropertiesLoginModule extends PropertiesLoader implements AuditLogi
} }
try { try {
hashProcessor = PasswordMaskingUtil.getHashProcessor(password); userHashProcessor = PasswordMaskingUtil.getHashProcessor(password, hashProcessor);
} catch (Exception e) { } catch (Exception e) {
throw new FailedLoginException("Failed to get hash processor"); 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); throw new FailedLoginException("Password does not match for user: " + user);
} }
loginSucceeded = true; loginSucceeded = true;

View File

@ -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 With this configuration, both passwords in ra.xml and all of its MDBs will have
to be in masked form. 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 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 (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 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 existing libraries or you implement it yourself. All codecs must implement
the `org.apache.activemq.artemis.utils.SensitiveDataCodec<T>` interface: the `org.apache.activemq.artemis.utils.SensitiveDataCodec<String>` interface.
So a new codec would be defined like
```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
```java ```java
public class MyCodec implements SensitiveDataCodec<String> { public class MyCodec implements SensitiveDataCodec<String> {
@ -464,6 +469,12 @@ public class MyCodec implements SensitiveDataCodec<String> {
public void init(Map<String, String> params) { public void init(Map<String, String> params) {
// Initialization done here. It is called right after the codec has been created. // 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);
}
} }
``` ```

View File

@ -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 - `org.apache.activemq.jaas.properties.role` - the path to the file which
contains user and role properties 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 - `reload` - boolean flag; whether or not to reload the properties files when a
modification occurs; default is `false` modification occurs; default is `false`

View File

@ -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.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.CreateMessage; import org.apache.activemq.artemis.tests.util.CreateMessage;
import org.apache.activemq.artemis.utils.CompositeAddress; 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.artemis.utils.Wait;
import org.apache.activemq.command.ActiveMQQueue; import org.apache.activemq.command.ActiveMQQueue;
import org.junit.Assert; import org.junit.Assert;
@ -129,6 +130,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 @Test
public void testJAASSecurityManagerAuthenticationWithValidateUser() throws Exception { public void testJAASSecurityManagerAuthenticationWithValidateUser() throws Exception {
ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("PropertiesLogin"); ActiveMQJAASSecurityManager securityManager = new ActiveMQJAASSecurityManager("PropertiesLogin");
@ -2564,4 +2581,21 @@ public class SecurityTest extends ActiveMQTestBase {
fail("Invalid Exception type:" + e.getType()); 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());
}
}
} }

View File

@ -21,6 +21,13 @@ PropertiesLogin {
org.apache.activemq.jaas.properties.role="roles.properties"; 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 { LDAPLogin {
org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required

View File

@ -17,6 +17,7 @@
programmers=first programmers=first
accounting=second accounting=second
test=test
employees=first,second employees=first,second
a=a a=a
b=b b=b

View File

@ -17,6 +17,7 @@
first=secret first=secret
second=password second=password
test=ENC(drowssap)
a=a a=a
b=b b=b
x=x x=x