From bb84f679363f62e8b2663f63bf23f04133de481d Mon Sep 17 00:00:00 2001 From: Howard Gao Date: Thu, 18 Jan 2018 14:31:32 +0800 Subject: [PATCH] ARTEMIS-1600 Support masked passwords in bootstrap.xm and login.config We provide a feature to mask passwords in the configuration files. However, passwords in the bootstrap.xml (when the console is secured with HTTPS) cannot be masked. This enhancement has been opened to allow passwords in the bootstrap.xml to be masked using the built-in masking feature provided by the broker. Also the LDAPLoginModule configuration (in login.config) has a connection password attribute that also needs this mask support. In addition the ENC() syntax is supported for password masking to replace the old 'mask-password' flag. --- .../activemq/cli/test/HashUtilTest.java | 47 ++++++ .../artemis/utils/PasswordMaskingUtil.java | 48 +++++- .../artemis/utils/SecureHashProcessor.java | 7 +- .../artemis/utils/SensitiveDataCodec.java | 3 +- .../utils/MaskPasswordResolvingTest.java | 116 +++++++++++++ .../config/ActiveMQDefaultConfiguration.java | 4 +- .../client/ActiveMQClientMessageBundle.java | 6 - .../artemis/utils/ConfigurationHelper.java | 19 +-- artemis-dto/pom.xml | 8 + .../activemq/artemis/dto/WebServerDTO.java | 35 +++- .../jms/bridge/impl/JMSBridgeImpl.java | 27 +-- .../artemis/ra/ActiveMQRAProperties.java | 28 +--- .../artemis/ra/ActiveMQResourceAdapter.java | 5 +- .../artemis/ra/inflow/ActiveMQActivation.java | 18 +- .../artemis/core/config/Configuration.java | 4 +- .../core/config/impl/ConfigurationImpl.java | 23 ++- .../impl/FileSecurityConfiguration.java | 16 +- .../impl/FileConfigurationParser.java | 37 ++--- .../core/security/jaas/LDAPLoginModule.java | 37 ++++- .../artemis/utils/XMLConfigurationUtil.java | 2 +- .../impl/FileConfigurationParserTest.java | 82 +++++++++ .../jaas/LDAPLoginModuleMaskPasswordTest.java | 156 ++++++++++++++++++ .../jaas/PropertiesLoginModuleTest.java | 7 + .../src/test/resources/login.config | 59 +++++++ .../src/test/resources/users.properties | 1 + artemis-web/pom.xml | 7 + .../artemis/component/WebServerComponent.java | 4 +- .../cli/test/WebServerComponentTest.java | 51 +++++- .../test/resources/bootstrap_secure_web.xml | 34 ++++ .../src/test/resources/bootstrap_web.xml | 34 ++++ .../test/resources/bootstrap_web_codec.xml | 34 ++++ docs/user-manual/en/masking-passwords.md | 141 ++++++++++++++-- .../integration/ra/ResourceAdapterTest.java | 84 +++++++++- .../jms/bridge/impl/JMSBridgeImplTest.java | 39 +++++ 34 files changed, 1057 insertions(+), 166 deletions(-) create mode 100644 artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java create mode 100644 artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java create mode 100644 artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java create mode 100644 artemis-web/src/test/resources/bootstrap_secure_web.xml create mode 100644 artemis-web/src/test/resources/bootstrap_web.xml create mode 100644 artemis-web/src/test/resources/bootstrap_web_codec.xml diff --git a/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java b/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java new file mode 100644 index 0000000000..09a5208223 --- /dev/null +++ b/artemis-cli/src/test/java/org/apache/activemq/cli/test/HashUtilTest.java @@ -0,0 +1,47 @@ +/* + * 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 + * + * 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.cli.test; + +import org.apache.activemq.artemis.cli.commands.util.HashUtil; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class HashUtilTest { + + @Test + public void testDefaultHashFormat() throws Exception { + final String password = "helloworld"; + String hash = HashUtil.tryHash(new TestActionContext(), password); + String hashStr = PasswordMaskingUtil.unwrap(hash); + System.out.println("hashString: " + hashStr); + String[] parts = hashStr.split(":"); + assertEquals(3, parts.length); + //first part should be able to convert to an int + Integer.parseInt(parts[0]); + //second and third parts are all hex values + checkHexBytes(parts[1], parts[2]); + } + + private void checkHexBytes(String... parts) throws Exception { + for (String p : parts) { + assertTrue(p.matches("^[0-9A-F]+$")); + } + } +} 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 dbc20c54c0..be560465f1 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 @@ -28,14 +28,54 @@ import org.apache.activemq.artemis.logs.ActiveMQUtilBundle; public final class PasswordMaskingUtil { + public static final String BEGIN_ENC = "ENC("; + public static final String END_ENC = ")"; + private PasswordMaskingUtil() { } + /** + * This method deals with password masking and returns the password in its plain text form. + * @param maskPassword : explicit mask flag. If it's true, the password is interpreted as + * masked. If it is false, the password is interpreted as plain text. + * if it is null, the password will be interpreted as masked if the + * password is wrapped in ENC(), or as plain text otherwise. + * @param password : the original value of password string + * @param codecClass : the codec used to decode the password. Only when the password is interpreted + * as masked will this codec be used. Ignored otherwise. + * @return + */ + public static String resolveMask(Boolean maskPassword, String password, String codecClass) throws Exception { + String plainText = password; + if (maskPassword == null) { + if (isEncMasked(password)) { + //masked + String bareMaskedPassword = unwrap(password); + plainText = getCodec(codecClass).decode(bareMaskedPassword); + } + } else if (maskPassword) { + plainText = getCodec(codecClass).decode(password); + } + return plainText; + } + + public static boolean isEncMasked(String password) { + return (password.startsWith(BEGIN_ENC) && password.endsWith(END_ENC)); + } + + //remove ENC() from the password body + public static String unwrap(String password) { + return password.substring(4, password.length() - 1); + } + + public static String wrap(String password) { + return BEGIN_ENC + password + END_ENC; + } + private static final class LazyPlainTextProcessorHolder { private LazyPlainTextProcessorHolder() { - } private static final HashProcessor INSTANCE = new NoHashProcessor(); @@ -83,7 +123,7 @@ public final class PasswordMaskingUtil { } private static boolean isEncoded(String storedPassword) { - return storedPassword == null || (storedPassword.startsWith(SecureHashProcessor.BEGIN_HASH) && storedPassword.endsWith(SecureHashProcessor.END_HASH)); + return storedPassword == null || (isEncMasked(storedPassword)); } public static HashProcessor getHashProcessor() { @@ -107,6 +147,10 @@ public final class PasswordMaskingUtil { public static SensitiveDataCodec getCodec(String codecDesc) throws ActiveMQException { SensitiveDataCodec codecInstance; + if (codecDesc == null) { + return getDefaultCodec(); + } + // semi colons String[] parts = codecDesc.split(";"); if (parts.length < 1) 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 8e9cd5c0f9..a17cfd4a2e 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,6 @@ package org.apache.activemq.artemis.utils; public class SecureHashProcessor implements HashProcessor { - public static final String BEGIN_HASH = "ENC("; - public static final String END_HASH = ")"; - private DefaultSensitiveStringCodec codec; public SecureHashProcessor(DefaultSensitiveStringCodec codec) { @@ -29,13 +26,13 @@ public class SecureHashProcessor implements HashProcessor { @Override public String hash(String plainText) throws Exception { - return BEGIN_HASH + codec.encode(plainText) + END_HASH; + return PasswordMaskingUtil.wrap(codec.encode(plainText)); } @Override //storedValue must take form of ENC(...) public boolean compare(char[] inputValue, String storedValue) { - String storedHash = storedValue.substring(4, storedValue.length() - 2); + String storedHash = storedValue.substring(4, storedValue.length() - 1); return codec.verify(inputValue, storedHash); } } 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 d5859763b2..cbd17e5ad0 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 @@ -31,5 +31,6 @@ public interface SensitiveDataCodec { T encode(Object secret) throws Exception; - void init(Map params) throws Exception; + default void init(Map params) throws Exception { + }; } diff --git a/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java new file mode 100644 index 0000000000..5a99ccc689 --- /dev/null +++ b/artemis-commons/src/test/java/org/apache/activemq/artemis/utils/MaskPasswordResolvingTest.java @@ -0,0 +1,116 @@ +/* + * 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 + * + * 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 org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class MaskPasswordResolvingTest { + + private static final String plainPassword = "password"; + private static final String defaultMaskPassword = "defaultmasked"; + private static final String customizedCodecPassword = "secret"; + private static final String oldDefaultMaskedPassword = "oldmasked"; + private static final String oldCustomizedCodecPassword = "secret"; + private static final String oldExplicitPlainPassword = "PASSWORD"; + + @Parameterized.Parameters(name = "mask({0})password({1})codec({2})") + public static Collection params() { + return Arrays.asList(new Object[][]{{null, plainPassword, null}, + {null, "ENC(3bdfd94fe8cdf710e7fefa72f809ea90)", null}, + {null, "ENC(momsword)", "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}, + {true, "662d05f5a83f9e073af6b8dc081d34aa", null}, + {true, "momsword", "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}, + {false, oldExplicitPlainPassword, null}, + {false, oldExplicitPlainPassword, "org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec"}}); + } + + private Boolean maskPassword; + private String password; + private String codec; + + public MaskPasswordResolvingTest(Boolean maskPassword, String password, String codec) { + this.maskPassword = maskPassword; + this.password = password; + this.codec = codec; + } + + @Test + public void testPasswordResolving() throws Exception { + String resolved = PasswordMaskingUtil.resolveMask(maskPassword, password, codec); + System.out.println("resolved: " + resolved); + checkResult(resolved); + } + + private void checkResult(String resolved) throws Exception { + if (this.maskPassword == null) { + if (PasswordMaskingUtil.isEncMasked(this.password)) { + if (this.codec != null) { + assertEquals(customizedCodecPassword, resolved); + } else { + assertEquals(defaultMaskPassword, resolved); + } + } else { + assertEquals(plainPassword, resolved); + } + } else { + if (this.maskPassword) { + if (this.codec != null) { + assertEquals(oldCustomizedCodecPassword, resolved); + } else { + assertEquals(oldDefaultMaskedPassword, resolved); + } + } else { + assertEquals(oldExplicitPlainPassword, resolved); + } + } + } + + public static class SimplePasswordCodec implements SensitiveDataCodec { + + private Map passwordBook = new HashMap<>(); + + public SimplePasswordCodec() { + passwordBook.put("momsword", "secret"); + passwordBook.put("youneverknow", "keypass"); + passwordBook.put("youcanguess", "trustpass"); + } + + @Override + public String decode(Object mask) throws Exception { + String password = passwordBook.get(mask); + if (password == null) { + throw new IllegalArgumentException("I don't know the password " + mask); + } + return password; + } + + @Override + public String encode(Object secret) throws Exception { + return null; + } + } +} diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java index 9b5d75d322..fecf71c259 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/api/config/ActiveMQDefaultConfiguration.java @@ -179,7 +179,7 @@ public final class ActiveMQDefaultConfiguration { private static String DEFAULT_CLUSTER_PASSWORD = "CHANGE ME!!"; // This option controls whether passwords in server configuration need be masked. If set to "true" the passwords are masked. - private static boolean DEFAULT_MASK_PASSWORD = false; + private static Boolean DEFAULT_MASK_PASSWORD = null; // true means that the management API is available via JMX private static boolean DEFAULT_JMX_MANAGEMENT_ENABLED = true; @@ -622,7 +622,7 @@ public final class ActiveMQDefaultConfiguration { /** * This option controls whether passwords in server configuration need be masked. If set to "true" the passwords are masked. */ - public static boolean isDefaultMaskPassword() { + public static Boolean isDefaultMaskPassword() { return DEFAULT_MASK_PASSWORD; } diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java index 64107b93d7..bb88e6d22b 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/client/ActiveMQClientMessageBundle.java @@ -178,9 +178,6 @@ public interface ActiveMQClientMessageBundle { @Message(id = 119043, value = "Invalid argument null handler") IllegalArgumentException nullHandler(); - @Message(id = 119044, value = "No available codec to decode password!") - IllegalArgumentException noCodec(); - @Message(id = 119045, value = "the first node to be compared is null") IllegalArgumentException firstNodeNull(); @@ -214,9 +211,6 @@ public interface ActiveMQClientMessageBundle { @Message(id = 119055, value = "Element {0} requires a valid Long value, but ''{1}'' cannot be parsed as a Long", format = Message.Format.MESSAGE_FORMAT) IllegalArgumentException mustBeLong(Node element, String value); - @Message(id = 119056, value = "Failed to get decoder") - IllegalArgumentException failedToGetDecoder(@Cause Exception e); - @Message(id = 119057, value = "Error decoding password") IllegalArgumentException errordecodingPassword(@Cause Exception e); diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java index a205c191cf..4367318ba3 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/utils/ConfigurationHelper.java @@ -20,7 +20,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.core.client.ActiveMQClientLogger; import org.apache.activemq.artemis.core.client.ActiveMQClientMessageBundle; @@ -164,25 +163,9 @@ public class ConfigurationHelper { String value = prop.toString(); Boolean useMask = (Boolean) props.get(defaultMaskPassword); - if (useMask == null || (!useMask)) { - return value; - } - final String classImpl = (String) props.get(defaultPasswordCodec); - - if (classImpl == null) { - throw ActiveMQClientMessageBundle.BUNDLE.noCodec(); - } - - SensitiveDataCodec codec = null; try { - codec = PasswordMaskingUtil.getCodec(classImpl); - } catch (ActiveMQException e1) { - throw ActiveMQClientMessageBundle.BUNDLE.failedToGetDecoder(e1); - } - - try { - return codec.decode(value); + return PasswordMaskingUtil.resolveMask(useMask, value, classImpl); } catch (Exception e) { throw ActiveMQClientMessageBundle.BUNDLE.errordecodingPassword(e); } diff --git a/artemis-dto/pom.xml b/artemis-dto/pom.xml index 05de601472..f875957d9a 100644 --- a/artemis-dto/pom.xml +++ b/artemis-dto/pom.xml @@ -31,6 +31,14 @@ ${project.basedir}/.. + + + org.apache.activemq + artemis-commons + ${project.version} + + + diff --git a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java index 4553e0aa6e..3e1526184f 100644 --- a/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java +++ b/artemis-dto/src/main/java/org/apache/activemq/artemis/dto/WebServerDTO.java @@ -16,6 +16,8 @@ */ package org.apache.activemq.artemis.dto; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; + import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; @@ -37,21 +39,44 @@ public class WebServerDTO extends ComponentDTO { public Boolean clientAuth; @XmlAttribute - public String keyStorePath; + public String passwordCodec; @XmlAttribute - public String keyStorePassword; + public String keyStorePath; @XmlAttribute public String trustStorePath; - @XmlAttribute - public String trustStorePassword; - @XmlElementRef public List apps; + @XmlAttribute + private String keyStorePassword; + + @XmlAttribute + private String trustStorePassword; + public WebServerDTO() { componentClassName = "org.apache.activemq.artemis.component.WebServerComponent"; } + + public String getKeyStorePassword() throws Exception { + return getPassword(this.keyStorePassword); + } + + private String getPassword(String password) throws Exception { + return PasswordMaskingUtil.resolveMask(null, password, this.passwordCodec); + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public String getTrustStorePassword() throws Exception { + return getPassword(this.trustStorePassword); + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } } diff --git a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java index b67b0b3c85..4d9b04e4d7 100644 --- a/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java +++ b/artemis-jms-server/src/main/java/org/apache/activemq/artemis/jms/bridge/impl/JMSBridgeImpl.java @@ -69,9 +69,7 @@ import org.apache.activemq.artemis.jms.server.ActiveMQJMSServerBundle; import org.apache.activemq.artemis.service.extensions.ServiceUtils; import org.apache.activemq.artemis.service.extensions.xa.recovery.ActiveMQRegistry; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; -import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; public final class JMSBridgeImpl implements JMSBridge { @@ -168,7 +166,7 @@ public final class JMSBridgeImpl implements JMSBridge { private ObjectName objectName; - private boolean useMaskedPassword = false; + private Boolean useMaskedPassword; private String passwordCodec; @@ -440,25 +438,16 @@ public final class JMSBridgeImpl implements JMSBridge { } private void initPasswords() throws ActiveMQException { - if (useMaskedPassword) { - SensitiveDataCodec codecInstance = new DefaultSensitiveStringCodec(); - - if (passwordCodec != null) { - codecInstance = PasswordMaskingUtil.getCodec(passwordCodec); + try { + if (this.sourcePassword != null) { + sourcePassword = PasswordMaskingUtil.resolveMask(useMaskedPassword, sourcePassword, passwordCodec); } - try { - if (this.sourcePassword != null) { - sourcePassword = codecInstance.decode(sourcePassword); - } - - if (this.targetPassword != null) { - targetPassword = codecInstance.decode(targetPassword); - } - } catch (Exception e) { - throw ActiveMQJMSServerBundle.BUNDLE.errorDecodingPassword(e); + if (this.targetPassword != null) { + targetPassword = PasswordMaskingUtil.resolveMask(useMaskedPassword, targetPassword, passwordCodec); } - + } catch (Exception e) { + throw ActiveMQJMSServerBundle.BUNDLE.errorDecodingPassword(e); } } diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java index dffd5b3afb..e56e80d991 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQRAProperties.java @@ -20,9 +20,7 @@ import java.io.Serializable; import java.util.Hashtable; import org.apache.activemq.artemis.api.core.ActiveMQException; -import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; /** * The RA default properties - these are set in the ra.xml file @@ -66,14 +64,12 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements private boolean useJNDI; - private boolean useMaskedPassword = false; + private Boolean useMaskedPassword = null; private String passwordCodec; private boolean initialized = false; - private transient SensitiveDataCodec codecInstance; - /** * Class used to get a JChannel */ @@ -217,11 +213,11 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements this.setupInterval = setupInterval; } - public boolean isUseMaskedPassword() { + public Boolean isUseMaskedPassword() { return useMaskedPassword; } - public void setUseMaskedPassword(boolean useMaskedPassword) { + public void setUseMaskedPassword(Boolean useMaskedPassword) { this.useMaskedPassword = useMaskedPassword; } @@ -243,27 +239,19 @@ public class ActiveMQRAProperties extends ConnectionFactoryProperties implements if (initialized) return; - if (useMaskedPassword) { - codecInstance = new DefaultSensitiveStringCodec(); - - if (passwordCodec != null) { - codecInstance = PasswordMaskingUtil.getCodec(passwordCodec); - } - + if (password != null) { try { - if (password != null) { - password = codecInstance.decode(password); - } + password = PasswordMaskingUtil.resolveMask(useMaskedPassword, password, passwordCodec); } catch (Exception e) { throw ActiveMQRABundle.BUNDLE.errorDecodingPassword(e); } - } + initialized = true; } - public SensitiveDataCodec getCodecInstance() { - return codecInstance; + public String getCodec() { + return passwordCodec; } public String getJgroupsChannelLocatorClass() { diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java index a43d37b128..684792eb01 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/ActiveMQResourceAdapter.java @@ -59,7 +59,6 @@ import org.apache.activemq.artemis.ra.inflow.ActiveMQActivationSpec; import org.apache.activemq.artemis.ra.recovery.RecoveryManager; import org.apache.activemq.artemis.service.extensions.ServiceUtils; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; import org.jboss.logging.Logger; import org.jgroups.JChannel; @@ -2037,8 +2036,8 @@ public class ActiveMQResourceAdapter implements ResourceAdapter, Serializable { managedConnectionFactories.add(activeMQRAManagedConnectionFactory); } - public SensitiveDataCodec getCodecInstance() { - return raProperties.getCodecInstance(); + public String getCodec() { + return raProperties.getCodec(); } public synchronized void closeConnectionFactory(ConnectionFactoryProperties properties) { diff --git a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java index dc4d648a9b..97498a2679 100644 --- a/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java +++ b/artemis-ra/src/main/java/org/apache/activemq/artemis/ra/inflow/ActiveMQActivation.java @@ -57,7 +57,7 @@ import org.apache.activemq.artemis.ra.ActiveMQRaUtils; import org.apache.activemq.artemis.ra.ActiveMQResourceAdapter; import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConfig; import org.apache.activemq.artemis.utils.FutureLatch; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.jboss.logging.Logger; /** @@ -148,16 +148,12 @@ public class ActiveMQActivation { logger.trace("constructor(" + ra + ", " + endpointFactory + ", " + spec + ")"); } - if (ra.isUseMaskedPassword()) { - String pass = spec.getOwnPassword(); - if (pass != null) { - SensitiveDataCodec codec = ra.getCodecInstance(); - - try { - spec.setPassword(codec.decode(pass)); - } catch (Exception e) { - throw new ResourceException(e); - } + String pass = spec.getOwnPassword(); + if (pass != null) { + try { + spec.setPassword(PasswordMaskingUtil.resolveMask(ra.isUseMaskedPassword(), pass, ra.getCodec())); + } catch (Exception e) { + throw new ResourceException(e); } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java index 6f44bbb5cc..f052248545 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java @@ -1000,12 +1000,12 @@ public interface Configuration { /** * Sets if passwords should be masked or not. True means the passwords should be masked. */ - Configuration setMaskPassword(boolean maskPassword); + Configuration setMaskPassword(Boolean maskPassword); /** * If passwords are masked. True means the passwords are masked. */ - boolean isMaskPassword(); + Boolean isMaskPassword(); /* * Whether or not that ActiveMQ Artemis should use all protocols available on the classpath. If false only the core protocol will diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index ff68f153b5..93085f9b24 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -249,7 +249,7 @@ public class ConfigurationImpl implements Configuration, Serializable { protected List connectorServiceConfigurations = new ArrayList<>(); - private boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword(); + private Boolean maskPassword = ActiveMQDefaultConfiguration.isDefaultMaskPassword(); private transient String passwordCodec; @@ -1461,12 +1461,12 @@ public class ConfigurationImpl implements Configuration, Serializable { } @Override - public boolean isMaskPassword() { + public Boolean isMaskPassword() { return maskPassword; } @Override - public ConfigurationImpl setMaskPassword(boolean maskPassword) { + public ConfigurationImpl setMaskPassword(Boolean maskPassword) { this.maskPassword = maskPassword; return this; } @@ -1673,7 +1673,6 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; if (asyncConnectionExecutionEnabled != other.asyncConnectionExecutionEnabled) return false; - if (bindingsDirectory == null) { if (other.bindingsDirectory != null) return false; @@ -1689,11 +1688,13 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!broadcastGroupConfigurations.equals(other.broadcastGroupConfigurations)) return false; + if (clusterConfigurations == null) { if (other.clusterConfigurations != null) return false; } else if (!clusterConfigurations.equals(other.clusterConfigurations)) return false; + if (clusterPassword == null) { if (other.clusterPassword != null) return false; @@ -1720,6 +1721,7 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; if (createJournalDir != other.createJournalDir) return false; + if (discoveryGroupConfigurations == null) { if (other.discoveryGroupConfigurations != null) return false; @@ -1801,8 +1803,15 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!managementNotificationAddress.equals(other.managementNotificationAddress)) return false; - if (maskPassword != other.maskPassword) - return false; + + if (this.maskPassword == null) { + if (other.maskPassword != null) + return false; + } else { + if (!this.maskPassword.equals(other.maskPassword)) + return false; + } + if (maxConcurrentPageIO != other.maxConcurrentPageIO) return false; if (memoryMeasureInterval != other.memoryMeasureInterval) @@ -1824,7 +1833,6 @@ public class ConfigurationImpl implements Configuration, Serializable { return false; } else if (!name.equals(other.name)) return false; - if (outgoingInterceptorClassNames == null) { if (other.outgoingInterceptorClassNames != null) return false; @@ -1881,6 +1889,7 @@ public class ConfigurationImpl implements Configuration, Serializable { if (journalDatasync != other.journalDatasync) { return false; } + if (globalMaxSize != null && !globalMaxSize.equals(other.globalMaxSize)) { return false; } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java index d778aa5643..b33a41988c 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/FileSecurityConfiguration.java @@ -22,7 +22,6 @@ import java.util.Set; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; @Deprecated public class FileSecurityConfiguration extends SecurityConfiguration { @@ -31,7 +30,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { private final String rolesUrl; - private boolean maskPassword; + private Boolean maskPassword; private String passwordCodec; @@ -65,14 +64,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { if (started) { return; } - SensitiveDataCodec codec = null; - if (maskPassword) { - if (passwordCodec != null) { - codec = PasswordMaskingUtil.getDefaultCodec(); - } else { - codec = PasswordMaskingUtil.getCodec(passwordCodec); - } - } + URL theUsersUrl = getClass().getClassLoader().getResource(usersUrl); if (theUsersUrl == null) { @@ -94,9 +86,7 @@ public class FileSecurityConfiguration extends SecurityConfiguration { for (String username : keys) { String password = userProps.getProperty(username); - if (codec != null) { - password = codec.decode(password); - } + password = PasswordMaskingUtil.resolveMask(this.maskPassword, password, passwordCodec); addUser(username, password); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java index 9b9050b47d..14b002501b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java @@ -80,7 +80,6 @@ import org.apache.activemq.artemis.utils.ByteUtil; import org.apache.activemq.artemis.utils.ClassloadingUtil; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; import org.apache.activemq.artemis.utils.PasswordMaskingUtil; -import org.apache.activemq.artemis.utils.SensitiveDataCodec; import org.apache.activemq.artemis.utils.XMLConfigurationUtil; import org.apache.activemq.artemis.utils.XMLUtil; import org.apache.activemq.artemis.utils.critical.CriticalAnalyzerPolicy; @@ -339,7 +338,7 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { config.setManagementNotificationAddress(new SimpleString(getString(e, "management-notification-address", config.getManagementNotificationAddress().toString(), Validators.NOT_NULL_OR_EMPTY))); - config.setMaskPassword(getBoolean(e, "mask-password", false)); + config.setMaskPassword(getBoolean(e, "mask-password", null)); config.setPasswordCodec(getString(e, "password-codec", DefaultSensitiveStringCodec.class.getName(), Validators.NOT_NULL_OR_EMPTY)); @@ -369,15 +368,11 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { // parsing cluster password String passwordText = getString(e, "cluster-password", null, Validators.NO_CHECK); - final boolean maskText = config.isMaskPassword(); + final Boolean maskText = config.isMaskPassword(); if (passwordText != null) { - if (maskText) { - SensitiveDataCodec codec = PasswordMaskingUtil.getCodec(config.getPasswordCodec()); - config.setClusterPassword(codec.decode(passwordText)); - } else { - config.setClusterPassword(passwordText); - } + String resolvedPassword = PasswordMaskingUtil.resolveMask(maskText, passwordText, config.getPasswordCodec()); + config.setClusterPassword(resolvedPassword); } config.setClusterUser(getString(e, "cluster-user", config.getClusterUser(), Validators.NO_CHECK)); @@ -1164,12 +1159,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { Map params = configurations.get(0).getParams(); - if (mainConfig.isMaskPassword()) { - params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); + params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); - if (mainConfig.getPasswordCodec() != null) { - params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); - } + if (mainConfig.getPasswordCodec() != null) { + params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); } return configurations.get(0); @@ -1187,12 +1180,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { Map params = configurations.get(0).getParams(); - if (mainConfig.isMaskPassword()) { - params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); + params.put(ActiveMQDefaultConfiguration.getPropMaskPassword(), mainConfig.isMaskPassword()); - if (mainConfig.getPasswordCodec() != null) { - params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); - } + if (mainConfig.getPasswordCodec() != null) { + params.put(ActiveMQDefaultConfiguration.getPropPasswordCodec(), mainConfig.getPasswordCodec()); } return configurations.get(0); @@ -1720,9 +1711,6 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { NodeList clusterPassNodes = brNode.getElementsByTagName("password"); String password = null; - boolean maskPassword = mainConfig.isMaskPassword(); - - SensitiveDataCodec codec = null; if (clusterPassNodes.getLength() > 0) { Node passNode = clusterPassNodes.item(0); @@ -1730,10 +1718,7 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { } if (password != null) { - if (maskPassword) { - codec = PasswordMaskingUtil.getCodec(mainConfig.getPasswordCodec()); - password = codec.decode(password); - } + password = PasswordMaskingUtil.resolveMask(mainConfig.isMaskPassword(), password, mainConfig.getPasswordCodec()); } else { password = ActiveMQDefaultConfiguration.getDefaultClusterPassword(); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java index a1ba4e743a..e24f4f6dc2 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java @@ -58,6 +58,7 @@ import java.util.Queue; import java.util.Set; import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.jboss.logging.Logger; public class LDAPLoginModule implements LoginModule { @@ -83,6 +84,8 @@ public class LDAPLoginModule implements LoginModule { private static final String SASL_LOGIN_CONFIG_SCOPE = "saslLoginConfigScope"; private static final String AUTHENTICATE_USER = "authenticateUser"; private static final String REFERRAL = "referral"; + private static final String MASK_PASSWORD = "maskPassword"; + private static final String PASSWORD_CODEC = "passwordCodec"; protected DirContext context; @@ -97,6 +100,8 @@ public class LDAPLoginModule implements LoginModule { private boolean isRoleAttributeSet = false; private String roleAttributeName = null; + private String codecClass = null; + @Override public void initialize(Subject subject, CallbackHandler callbackHandler, @@ -105,12 +110,38 @@ public class LDAPLoginModule implements LoginModule { this.subject = subject; this.handler = callbackHandler; - config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL))}; + config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)), + new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)), + new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)), + new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)), + new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)), + new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)), + new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)), + new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)), + new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)), + new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)), + new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)), + new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)), + new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)), + new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)), + new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)), + new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)), + new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL))}; + if (isLoginPropertySet(AUTHENTICATE_USER)) { authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER)); } isRoleAttributeSet = isLoginPropertySet(ROLE_NAME); roleAttributeName = getLDAPPropertyValue(ROLE_NAME); + codecClass = (String) options.get(PASSWORD_CODEC); + } + + private String getPlainPassword(String password) { + try { + return PasswordMaskingUtil.resolveMask(null, password, codecClass); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to decode password", e); + } } @Override @@ -511,7 +542,7 @@ public class LDAPLoginModule implements LoginModule { context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); } if (isLoginPropertySet(CONNECTION_PASSWORD)) { - context.addToEnvironment(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); } else { context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); } @@ -575,7 +606,7 @@ public class LDAPLoginModule implements LoginModule { } if (isLoginPropertySet(CONNECTION_PASSWORD)) { - env.put(Context.SECURITY_CREDENTIALS, getLDAPPropertyValue(CONNECTION_PASSWORD)); + env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD))); } else { throw new NamingException("Empty password is not allowed"); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java index 7ce52800c1..29cfef9d57 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/utils/XMLConfigurationUtil.java @@ -116,7 +116,7 @@ public class XMLConfigurationUtil { return getTextBytesAsLongBytes(e, name, def, validator).intValue(); } - public static final Boolean getBoolean(final Element e, final String name, final boolean def) { + public static final Boolean getBoolean(final Element e, final String name, final Boolean def) { NodeList nl = e.getElementsByTagName(name); if (nl.getLength() > 0) { return XMLUtil.parseBoolean(nl.item(0)); diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java index 94d64ecc43..564372deff 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/FileConfigurationParserTest.java @@ -19,10 +19,12 @@ package org.apache.activemq.artemis.core.config.impl; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.FileDeploymentManager; import org.apache.activemq.artemis.core.config.HAPolicyConfiguration; @@ -32,6 +34,7 @@ import org.apache.activemq.artemis.core.deployers.impl.FileConfigurationParser; import org.apache.activemq.artemis.core.server.ActiveMQServer; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.junit.Assert; import org.junit.Test; @@ -190,6 +193,85 @@ public class FileConfigurationParserTest extends ActiveMQTestBase { assertEquals("newpassword", config.getClusterPassword()); } + @Test + public void testParsingDefaultServerConfigWithENCMaskedPwd() throws Exception { + FileConfigurationParser parser = new FileConfigurationParser(); + + String configStr = firstPart + lastPart; + ByteArrayInputStream input = new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8)); + + Configuration config = parser.parseMainConfig(input); + + String clusterPassword = config.getClusterPassword(); + + assertEquals(ActiveMQDefaultConfiguration.getDefaultClusterPassword(), clusterPassword); + + //if we add cluster-password, it should be default plain text + String clusterPasswordPart = "ENC(5aec0780b12bf225a13ab70c6c76bc8e)"; + + configStr = firstPart + clusterPasswordPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("helloworld", config.getClusterPassword()); + + //if we add mask, it should be able to decode correctly + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + String mask = (String) codec.encode("helloworld"); + + clusterPasswordPart = "" + PasswordMaskingUtil.wrap(mask) + ""; + + configStr = firstPart + clusterPasswordPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("helloworld", config.getClusterPassword()); + + //if we change key, it should be able to decode correctly + codec = new DefaultSensitiveStringCodec(); + Map prop = new HashMap<>(); + prop.put("key", "newkey"); + codec.init(prop); + + mask = (String) codec.encode("newpassword"); + + clusterPasswordPart = "" + PasswordMaskingUtil.wrap(mask) + ""; + + String codecPart = "" + "org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec" + + ";key=newkey"; + + configStr = firstPart + clusterPasswordPart + codecPart + lastPart; + + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + assertEquals("newpassword", config.getClusterPassword()); + + configStr = firstPart + bridgePart + lastPart; + config = parser.parseMainConfig(new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8))); + + List bridgeConfigs = config.getBridgeConfigurations(); + assertEquals(1, bridgeConfigs.size()); + + BridgeConfiguration bconfig = bridgeConfigs.get(0); + + assertEquals("helloworld", bconfig.getPassword()); + } + + private static String bridgePart = "\n" + + " \n" + + " sausage-factory\n" + + " mincing-machine\n" + + " \n" + + " org.apache.activemq.artemis.jms.example.HatColourChangeTransformer\n" + + " -1\n" + + " bridge-user" + + " ENC(5aec0780b12bf225a13ab70c6c76bc8e)" + + " \n" + + " remote-connector\n" + + " \n" + + " \n" + + "\n"; + private static String firstPart = "" + "\n" + "ActiveMQ.main.config" + "\n" + "org.apache.activemq.artemis.integration.logging.Log4jLogDelegateFactory" + "\n" + diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java new file mode 100644 index 0000000000..cfb9934b46 --- /dev/null +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleMaskPasswordTest.java @@ -0,0 +1,156 @@ +/* + * 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 + * + * 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.core.security.jaas; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.apache.directory.server.core.integ.FrameworkRunner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.FailedLoginException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(FrameworkRunner.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 1024)}) +@ApplyLdifFiles("test.ldif") +public class LDAPLoginModuleMaskPasswordTest extends AbstractLdapTestUnit { + + private final String loginConfigSysPropName = "java.security.auth.login.config"; + private String oldLoginConfig; + + @Before + public void setLoginConfigSysProperty() { + oldLoginConfig = System.getProperty(loginConfigSysPropName, null); + System.setProperty(loginConfigSysPropName, "src/test/resources/login.config"); + } + + @After + public void resetLoginConfigSysProperty() { + if (oldLoginConfig != null) { + System.setProperty(loginConfigSysPropName, oldLoginConfig); + } + } + + @Test + public void testLoginMaskedPassword() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginMaskedPassword", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + context.login(); + context.logout(); + } + + @Test + public void testLoginMaskedPasswordUnauthenticated() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginMaskedPassword", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("nosecret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + try { + context.login(); + } catch (FailedLoginException le) { + assertEquals(le.getMessage(), "Password does not match for user: first"); + return; + } + fail("Should have failed authenticating"); + } + + @Test + public void testLoginExternalCodec() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + + context.login(); + context.logout(); + } + + @Test + public void testLoginExternalCodec2() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec2", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("secret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + + context.login(); + context.logout(); + } + + @Test + public void testLoginExternalCodecUnauthenticated() throws LoginException { + LoginContext context = new LoginContext("LDAPLoginExternalPasswordCodec", callbacks -> { + for (int i = 0; i < callbacks.length; i++) { + if (callbacks[i] instanceof NameCallback) { + ((NameCallback) callbacks[i]).setName("first"); + } else if (callbacks[i] instanceof PasswordCallback) { + ((PasswordCallback) callbacks[i]).setPassword("nosecret".toCharArray()); + } else { + throw new UnsupportedCallbackException(callbacks[i]); + } + } + }); + try { + context.login(); + } catch (FailedLoginException le) { + assertEquals(le.getMessage(), "Password does not match for user: first"); + return; + } + fail("Should have failed authenticating"); + } +} diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java index 1ab5dbaec5..eb6c9fa3c1 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/PropertiesLoginModuleTest.java @@ -76,6 +76,13 @@ public class PropertiesLoginModuleTest extends Assert { assertEquals("Should have zero principals", 0, subject.getPrincipals().size()); } + @Test + public void testLoginMasked() throws LoginException { + LoginContext context = new LoginContext("PropertiesLogin", new UserPassHandler("third", "helloworld")); + context.login(); + context.logout(); + } + @Test public void testLoginReload() throws Exception { File targetPropDir = new File("target/loginReloadTest"); diff --git a/artemis-server/src/test/resources/login.config b/artemis-server/src/test/resources/login.config index 997bfe5bd6..8e531caf06 100644 --- a/artemis-server/src/test/resources/login.config +++ b/artemis-server/src/test/resources/login.config @@ -125,3 +125,62 @@ OpenLdapConfiguration { roleSearchSubtree=true ; }; + +LDAPLoginMaskedPassword { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-41e444c3ed07d6dd)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=cn + roleSearchMatching="(member=uid={1},ou=system)" + roleSearchSubtree=false + ; +}; + +LDAPLoginExternalPasswordCodec { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + passwordCodec="org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=helloworld" + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-170b9ef34d79ed12)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; + +LDAPLoginExternalPasswordCodec2 { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + passwordCodec="org.apache.activemq.artemis.utils.MaskPasswordResolvingTest$SimplePasswordCodec" + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(momsword)" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; diff --git a/artemis-server/src/test/resources/users.properties b/artemis-server/src/test/resources/users.properties index 1087b0b3f1..c1a6b4739d 100644 --- a/artemis-server/src/test/resources/users.properties +++ b/artemis-server/src/test/resources/users.properties @@ -17,3 +17,4 @@ first=secret second=password +third=ENC(1024:439F45267508BB4F02150B9AAFB8D774AB7FCDDE9B19D096F318787487F7BD17:78818E53AEE8AF26A34A38C1498D1CDA5636861D2FE9804FEDE3656D0BC05696191A027F095DF109EC8F6385FAE9971915449EC808945A0F5907B29D5F9D44B7) \ No newline at end of file diff --git a/artemis-web/pom.xml b/artemis-web/pom.xml index a9be5c0351..4edc47aec1 100644 --- a/artemis-web/pom.xml +++ b/artemis-web/pom.xml @@ -68,6 +68,13 @@ artemis-commons ${project.version} + + org.apache.activemq + artemis-commons + ${project.version} + tests + test + io.netty netty-buffer diff --git a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java index 9964c719c9..d85d621807 100644 --- a/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java +++ b/artemis-web/src/main/java/org/apache/activemq/artemis/component/WebServerComponent.java @@ -68,12 +68,12 @@ public class WebServerComponent implements ExternalComponent { if ("https".equals(scheme)) { SslContextFactory sslFactory = new SslContextFactory(); sslFactory.setKeyStorePath(webServerConfig.keyStorePath == null ? artemisInstance + "/etc/keystore.jks" : webServerConfig.keyStorePath); - sslFactory.setKeyStorePassword(webServerConfig.keyStorePassword == null ? "password" : webServerConfig.keyStorePassword); + sslFactory.setKeyStorePassword(webServerConfig.getKeyStorePassword() == null ? "password" : webServerConfig.getKeyStorePassword()); if (webServerConfig.clientAuth != null) { sslFactory.setNeedClientAuth(webServerConfig.clientAuth); if (webServerConfig.clientAuth) { sslFactory.setTrustStorePath(webServerConfig.trustStorePath); - sslFactory.setTrustStorePassword(webServerConfig.trustStorePassword); + sslFactory.setTrustStorePassword(webServerConfig.getTrustStorePassword()); } } diff --git a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java index 00691ed498..11781473af 100644 --- a/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java +++ b/artemis-web/src/test/java/org/apache/activemq/cli/test/WebServerComponentTest.java @@ -18,6 +18,7 @@ package org.apache.activemq.cli.test; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -43,9 +44,11 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; +import org.apache.activemq.artemis.cli.factory.xml.XmlBrokerFactoryHandler; import org.apache.activemq.artemis.component.WebServerComponent; import org.apache.activemq.artemis.core.remoting.impl.ssl.SSLSupport; import org.apache.activemq.artemis.core.server.ActiveMQComponent; +import org.apache.activemq.artemis.dto.BrokerDTO; import org.apache.activemq.artemis.dto.WebServerDTO; import org.junit.After; import org.junit.Assert; @@ -163,7 +166,7 @@ public class WebServerComponentTest extends Assert { webServerDTO.bind = "https://localhost:0"; webServerDTO.path = "webapps"; webServerDTO.keyStorePath = "./src/test/resources/server.keystore"; - webServerDTO.keyStorePassword = "password"; + webServerDTO.setKeyStorePassword("password"); WebServerComponent webServerComponent = new WebServerComponent(); Assert.assertFalse(webServerComponent.isStarted()); @@ -174,7 +177,7 @@ public class WebServerComponentTest extends Assert { // Make the connection attempt. String keyStoreProvider = "JKS"; - SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword, keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword); + SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword(), keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword()); SSLEngine engine = context.createSSLEngine(); engine.setUseClientMode(true); @@ -215,10 +218,10 @@ public class WebServerComponentTest extends Assert { webServerDTO.bind = "https://localhost:0"; webServerDTO.path = "webapps"; webServerDTO.keyStorePath = "./src/test/resources/server.keystore"; - webServerDTO.keyStorePassword = "password"; + webServerDTO.setKeyStorePassword("password"); webServerDTO.clientAuth = true; webServerDTO.trustStorePath = "./src/test/resources/server.keystore"; - webServerDTO.trustStorePassword = "password"; + webServerDTO.setTrustStorePassword("password"); WebServerComponent webServerComponent = new WebServerComponent(); Assert.assertFalse(webServerComponent.isStarted()); @@ -229,7 +232,7 @@ public class WebServerComponentTest extends Assert { // Make the connection attempt. String keyStoreProvider = "JKS"; - SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.keyStorePassword, keyStoreProvider, webServerDTO.trustStorePath, webServerDTO.trustStorePassword); + SSLContext context = SSLSupport.createContext(keyStoreProvider, webServerDTO.keyStorePath, webServerDTO.getKeyStorePassword(), keyStoreProvider, webServerDTO.trustStorePath, webServerDTO.getTrustStorePassword()); SSLEngine engine = context.createSSLEngine(); engine.setUseClientMode(true); @@ -264,6 +267,44 @@ public class WebServerComponentTest extends Assert { Assert.assertFalse(webServerComponent.isStarted()); } + @Test + public void testDefaultMaskPasswords() throws Exception { + File bootstrap = new File("./target/test-classes/bootstrap_web.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertNull(broker.web.passwordCodec); + } + + @Test + public void testMaskPasswords() throws Exception { + final String keyPassword = "keypass"; + final String trustPassword = "trustpass"; + File bootstrap = new File("./target/test-classes/bootstrap_secure_web.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertEquals(keyPassword, broker.web.getKeyStorePassword()); + assertEquals(trustPassword, broker.web.getTrustStorePassword()); + } + + @Test + public void testMaskPasswordCodec() throws Exception { + final String keyPassword = "keypass"; + final String trustPassword = "trustpass"; + File bootstrap = new File("./target/test-classes/bootstrap_web_codec.xml"); + File brokerHome = new File("./target"); + XmlBrokerFactoryHandler xmlHandler = new XmlBrokerFactoryHandler(); + BrokerDTO broker = xmlHandler.createBroker(bootstrap.toURI(), brokerHome.getAbsolutePath(), brokerHome.getAbsolutePath(), brokerHome.toURI()); + assertNotNull(broker.web); + assertNotNull("password codec not picked up!", broker.web.passwordCodec); + + assertEquals(keyPassword, broker.web.getKeyStorePassword()); + assertEquals(trustPassword, broker.web.getTrustStorePassword()); + } + class ClientHandler extends SimpleChannelInboundHandler { private CountDownLatch latch; diff --git a/artemis-web/src/test/resources/bootstrap_secure_web.xml b/artemis-web/src/test/resources/bootstrap_secure_web.xml new file mode 100644 index 0000000000..affa9abb79 --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_secure_web.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/artemis-web/src/test/resources/bootstrap_web.xml b/artemis-web/src/test/resources/bootstrap_web.xml new file mode 100644 index 0000000000..847a869245 --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_web.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/artemis-web/src/test/resources/bootstrap_web_codec.xml b/artemis-web/src/test/resources/bootstrap_web_codec.xml new file mode 100644 index 0000000000..7b5351531a --- /dev/null +++ b/artemis-web/src/test/resources/bootstrap_web_codec.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/user-manual/en/masking-passwords.md b/docs/user-manual/en/masking-passwords.md index 4f435247f0..32329bacdc 100644 --- a/docs/user-manual/en/masking-passwords.md +++ b/docs/user-manual/en/masking-passwords.md @@ -18,12 +18,33 @@ Apache ActiveMQ Artemis provides a default password encoder and decoder. Optiona users can use or implement their own encoder and decoder for masking the passwords. +In general, a masked password can be identified using one of two ways. The first one +is the ENC() syntax, i.e. any string value wrapped in ENC() is to be treated as +a masked password. For example + +`ENC(xyz)` + +The above indicates that the password is masked and the masked value is `xyz`. + +The ENC() syntax is the preferred way to indicating a masked password and is +universally supported in every password configuration in Artemis. + +The other way is to use a `mask-password` attribute to tell that a password +in a configuration file should be treated as 'masked'. For example: + +``` +true +xyz +``` +This method is now deprecated and exists only to maintain backward-compatibility. +Newer configurations may not support it. + ### Password Masking in Server Configuration File #### General Masking Configuration -The server configuration file (i.e. broker.xml )has a property that defines the -default masking behaviors over the entire file scope. +Besides supporting the ENC() syntax, the server configuration file (i.e. broker.xml) +has a property that defines the default masking behaviors over the entire file scope. `mask-password`: this boolean type property indicates if a password should be masked or not. Set it to "true" if you want your passwords @@ -38,6 +59,8 @@ will be used. ##### cluster-password +If it is specified in ENC() syntax it will be treated as masked, or + If `mask-password` is `true` the `cluster-password` will be treated as masked. ##### Passwords in connectors and acceptors @@ -55,16 +78,16 @@ and `activemq.passwordcodec` respectively. The Netty and InVM implementations will use these as needed and any other implementations will have access to these to use if they so wish. +The preferred way, however, is to use the ENC() syntax. + ##### Passwords in bridge configurations Core Bridges are configured in the server configuration file and so the masking of its `password` properties follows the same rules as that of -`cluster-password`. +`cluster-password`. It supports ENC() syntax. -#### Examples - -The following table summarizes the relations among the above-mentioned -properties +For using 'mask-password' property, the following table summarizes the +relations among the above-mentioned properties mask-password | cluster-password | acceptor/connector passwords | bridge password :------------- | :---------------- | :--------------------------- | :--------------- @@ -72,7 +95,9 @@ properties false | plain text | plain text | plain text true | masked | masked | masked -Examples +It is recommended that you use the ENC() syntax for new applications/deployments. + +#### Examples Note: In the following examples if related attributed or properties are absent, it means they are not specified in the configure file. @@ -87,6 +112,14 @@ This indicates the cluster password is a plain text value ("bbc"). example 2 +```xml +ENC(xyz) +``` + +This indicates the cluster password is a masked value ("xyz"). + +example 3 + ```xml true 80cf731af62c290 @@ -97,27 +130,75 @@ use its built-in decoder to decode it. All other passwords in the configuration file, Connectors, Acceptors and Bridges, will also use masked passwords. +#### Passwords in bootstrap.xml + +The broker embeds a web-server for hosting some web applications such as a +management console. It is configured in bootstrap.xml as a web +component. The web server can be secured using https protocol, and it can be +configured with a keystore password and/or truststore password which by +default are specified in plain text forms. + +To mask these passwords you need to use ENC() syntax. The `mask-password` is +not supported here. + +You can also set the `passwordCodec` attribute if you want to use a password codec +other than the default one. For example + +```xml + + + +``` + ### Masking passwords in ActiveMQ Artemis JCA ResourceAdapter and MDB activation configurations Both ra.xml and MDB activation configuration have a `password` property -that can be masked. They are controlled by the following two optional -Resource Adapter properties in ra.xml: +that can be masked preferably using ENC() syntax. + +Alternatively it can use a optional attribute in ra.xml to indicate that a password +is masked: `UseMaskedPassword` -- If setting to "true" the passwords are masked. Default is false. +There is another property in ra.xml that can specify a codec: + `PasswordCodec` -- Class name and its parameters for the Decoder used to decode the masked password. Ignored if UseMaskedPassword is false. The format of this property is a full qualified class name optionally followed by key/value pairs. It is the same format as that for JMS Bridges. Example: +Example 1 Using the ENC() syntax: + +```xml + + password + String + ENC(xyz) + + + PasswordCodec + java.lang.String + com.foo.ADecoder;key=helloworld + +``` + +Example 2 Using the "UseMaskedPassword" property: + ```xml UseMaskedPassword boolean true + + password + String + xyz + PasswordCodec java.lang.String @@ -150,6 +231,43 @@ Passwords in `artemis-users.properties` are automatically detected as hashed or by looking for the syntax `ENC()`. The `mask-password` parameter does not need to be `true` to use hashed passwords here. +### Masking password in JAAS login config file (login.config) + +Artemis supports LDAP login modules to be configured in JAAS configuration +file (default name is `login.config`). When connecting to a LDAP server usually +you need to supply a connection password in the config file. By default this +password is in plain text form. + +To mask it you need to configure the passwords in your login module +using ENC() syntax. To specify a codec using the following property: + +`passwordCodec` - the password codec class name. (the default codec +will be used if it is absent) + +For example: + +``` +LDAPLoginExternalPasswordCodec { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword="ENC(-170b9ef34d79ed12)" + passwordCodec="org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec;key=helloworld" + connectionProtocol=s + authentication=simple + userBase="ou=system" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="ou=system" + roleName=dummyRoleName + roleSearchMatching="(uid={1})" + roleSearchSubtree=false + ; +}; +``` + ### Choosing a decoder for password masking As described in the previous sections, all password masking requires a @@ -206,8 +324,7 @@ pairs when configuring. For instance if your decoder needs say a Then configure your cluster-password like this: ```xml - true - masked_password + ENC(masked_password) ``` When Apache ActiveMQ Artemis reads the cluster-password it will initialize the diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java index c7a1474a8a..811cc29dba 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/ra/ResourceAdapterTest.java @@ -41,6 +41,7 @@ import org.apache.activemq.artemis.service.extensions.xa.recovery.XARecoveryConf import org.apache.activemq.artemis.tests.unit.ra.BootstrapContext; import org.apache.activemq.artemis.tests.unit.ra.MessageEndpointFactory; import org.apache.activemq.artemis.utils.DefaultSensitiveStringCodec; +import org.apache.activemq.artemis.utils.PasswordMaskingUtil; import org.junit.Test; public class ResourceAdapterTest extends ActiveMQRATestBase { @@ -565,7 +566,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); - String mask = (String) codec.encode("helloworld"); + String mask = codec.encode("helloworld"); qResourceAdapter.setUseMaskedPassword(true); qResourceAdapter.setPassword(mask); @@ -594,6 +595,41 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { assertTrue(endpoint.released); } + @Test + public void testMaskPasswordENC() throws Exception { + ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); + + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + String mask = codec.encode("helloworld"); + + qResourceAdapter.setPassword(PasswordMaskingUtil.wrap(mask)); + + qResourceAdapter.start(ctx); + + assertEquals("helloworld", qResourceAdapter.getPassword()); + + ActiveMQActivationSpec spec = new ActiveMQActivationSpec(); + spec.setResourceAdapter(qResourceAdapter); + spec.setUseJNDI(false); + spec.setDestinationType("javax.jms.Queue"); + spec.setDestination(MDBQUEUE); + + mask = codec.encode("mdbpassword"); + spec.setPassword(PasswordMaskingUtil.wrap(mask)); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + CountDownLatch latch = new CountDownLatch(1); + DummyMessageEndpoint endpoint = new DummyMessageEndpoint(latch); + DummyMessageEndpointFactory endpointFactory = new DummyMessageEndpointFactory(endpoint, false); + qResourceAdapter.endpointActivation(endpointFactory, spec); + + assertEquals("mdbpassword", spec.getPassword()); + + qResourceAdapter.stop(); + assertTrue(endpoint.released); + } + @Test public void testMaskPassword2() throws Exception { ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); @@ -609,7 +645,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { prop.put("key", "anotherkey"); codec.init(prop); - String mask = (String) codec.encode("helloworld"); + String mask = codec.encode("helloworld"); qResourceAdapter.setPassword(mask); @@ -623,7 +659,7 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { spec.setDestinationType("javax.jms.Queue"); spec.setDestination(MDBQUEUE); - mask = (String) codec.encode("mdbpassword"); + mask = codec.encode("mdbpassword"); spec.setPassword(mask); qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); CountDownLatch latch = new CountDownLatch(1); @@ -637,6 +673,48 @@ public class ResourceAdapterTest extends ActiveMQRATestBase { assertTrue(endpoint.released); } + @Test + public void testMaskPassword2ENC() throws Exception { + ActiveMQResourceAdapter qResourceAdapter = new ActiveMQResourceAdapter(); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + ActiveMQRATestBase.MyBootstrapContext ctx = new ActiveMQRATestBase.MyBootstrapContext(); + + qResourceAdapter.setPasswordCodec(DefaultSensitiveStringCodec.class.getName() + ";key=anotherkey"); + + DefaultSensitiveStringCodec codec = new DefaultSensitiveStringCodec(); + Map prop = new HashMap<>(); + + prop.put("key", "anotherkey"); + codec.init(prop); + + String mask = codec.encode("helloworld"); + + qResourceAdapter.setPassword(PasswordMaskingUtil.wrap(mask)); + + qResourceAdapter.start(ctx); + + assertEquals("helloworld", qResourceAdapter.getPassword()); + + ActiveMQActivationSpec spec = new ActiveMQActivationSpec(); + spec.setResourceAdapter(qResourceAdapter); + spec.setUseJNDI(false); + spec.setDestinationType("javax.jms.Queue"); + spec.setDestination(MDBQUEUE); + + mask = codec.encode("mdbpassword"); + spec.setPassword(PasswordMaskingUtil.wrap(mask)); + qResourceAdapter.setConnectorClassName(INVM_CONNECTOR_FACTORY); + CountDownLatch latch = new CountDownLatch(1); + DummyMessageEndpoint endpoint = new DummyMessageEndpoint(latch); + DummyMessageEndpointFactory endpointFactory = new DummyMessageEndpointFactory(endpoint, false); + qResourceAdapter.endpointActivation(endpointFactory, spec); + + assertEquals("mdbpassword", spec.getPassword()); + + qResourceAdapter.stop(); + assertTrue(endpoint.released); + } + @Test public void testConnectionParameterStringParsing() throws Exception { ActiveMQResourceAdapter resourceAdapter = new ActiveMQResourceAdapter(); diff --git a/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java b/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java index 9007bb40cc..f6a08a05fb 100644 --- a/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java +++ b/tests/timing-tests/src/test/java/org/apache/activemq/artemis/tests/timing/jms/bridge/impl/JMSBridgeImplTest.java @@ -287,6 +287,45 @@ public class JMSBridgeImplTest extends ActiveMQTestBase { * we receive only 1 message. The message is sent when the maxBatchTime * expires even if the maxBatchSize is not reached */ + @Test + public void testBridgeWithMaskPasswords() throws Exception { + + ConnectionFactoryFactory sourceCFF = JMSBridgeImplTest.newConnectionFactoryFactory(JMSBridgeImplTest.createConnectionFactory()); + ConnectionFactoryFactory targetCFF = JMSBridgeImplTest.newConnectionFactoryFactory(JMSBridgeImplTest.createConnectionFactory()); + DestinationFactory sourceDF = JMSBridgeImplTest.newDestinationFactory(ActiveMQJMSClient.createQueue(JMSBridgeImplTest.SOURCE)); + DestinationFactory targetDF = JMSBridgeImplTest.newDestinationFactory(ActiveMQJMSClient.createQueue(JMSBridgeImplTest.TARGET)); + TransactionManager tm = JMSBridgeImplTest.newTransactionManager(); + + JMSBridgeImpl bridge = new JMSBridgeImpl(); + Assert.assertNotNull(bridge); + + bridge.setSourceConnectionFactoryFactory(sourceCFF); + bridge.setSourceDestinationFactory(sourceDF); + bridge.setTargetConnectionFactoryFactory(targetCFF); + bridge.setTargetDestinationFactory(targetDF); + bridge.setFailureRetryInterval(10); + bridge.setMaxRetries(1); + bridge.setMaxBatchSize(1); + bridge.setMaxBatchTime(-1); + bridge.setTransactionManager(tm); + bridge.setQualityOfServiceMode(QualityOfServiceMode.AT_MOST_ONCE); + + bridge.setSourceUsername("sourceuser"); + bridge.setSourcePassword("ENC(5493dd76567ee5ec269d11823973462f)"); + bridge.setTargetUsername("targetuser"); + bridge.setTargetPassword("ENC(56a0db3b71043054269d11823973462f)"); + + Assert.assertFalse(bridge.isStarted()); + bridge.start(); + Assert.assertTrue(bridge.isStarted()); + + assertEquals("sourcepassword", bridge.getSourcePassword()); + assertEquals("targetpassword", bridge.getTargetPassword()); + + bridge.stop(); + Assert.assertFalse(bridge.isStarted()); + } + @Test public void testSendMessagesWhenMaxBatchTimeExpires() throws Exception { int maxBatchSize = 2;