You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by cl...@apache.org on 2021/04/08 23:58:39 UTC
[activemq-artemis] branch master updated: ARTEMIS-3106 Support for
SASL-SCRAM
This is an automated email from the ASF dual-hosted git repository.
clebertsuconic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git
The following commit(s) were added to refs/heads/master by this push:
new 5313a80 ARTEMIS-3106 Support for SASL-SCRAM
new c56912c This closes #3470
5313a80 is described below
commit 5313a800a37a4174a43289457a8934266b50f96b
Author: Christoph Läubrich <ch...@laeubi-soft.de>
AuthorDate: Thu Feb 25 06:42:17 2021 +0100
ARTEMIS-3106 Support for SASL-SCRAM
adds the implementation necessary to perform SASL-SCRAM authentication with ActiveMQ Artemis
---
artemis-protocols/artemis-amqp-protocol/pom.xml | 5 +
.../amqp/connect/AMQPBrokerConnection.java | 16 +-
.../protocol/amqp/sasl/scram/SCRAMClientSASL.java | 97 +
.../protocol/amqp/sasl/scram/SCRAMServerSASL.java | 163 ++
.../amqp/sasl/scram/SCRAMServerSASLFactory.java | 157 ++
.../sasl/scram/SHA256SCRAMServerSASLFactory.java | 30 +-
.../sasl/scram/SHA512SCRAMServerSASLFactory.java | 30 +-
.../amqp/sasl/scram/ScramClientFunctionality.java | 93 +
.../sasl/scram/ScramClientFunctionalityImpl.java | 215 ++
.../amqp/sasl/scram/ScramServerFunctionality.java | 98 +
.../sasl/scram/ScramServerFunctionalityImpl.java | 204 ++
...mq.artemis.protocol.amqp.sasl.ServerSASLFactory | 4 +-
.../artemis/protocol/amqp/sasl/SCRAMTest.java | 192 ++
...dule.java => AbstractPrincipalLoginModule.java} | 57 +-
.../{Krb5Callback.java => DigestCallback.java} | 27 +-
.../jaas/{Krb5Callback.java => HmacCallback.java} | 26 +-
.../core/security/jaas/JaasCallbackHandler.java | 30 +-
.../spi/core/security/jaas/Krb5LoginModule.java | 94 +-
.../{Krb5Callback.java => PrincipalsCallback.java} | 23 +-
.../{Krb5Callback.java => SCRAMLoginModule.java} | 25 +-
...b5Callback.java => SCRAMMechanismCallback.java} | 26 +-
.../security/jaas/SCRAMPropertiesLoginModule.java | 237 +++
.../artemis/spi/core/security/scram/SCRAM.java | 56 +
.../spi/core/security/scram/ScramException.java | 44 +
.../spi/core/security/scram/ScramUtils.java | 264 +++
.../spi/core/security/scram/StringPrep.java | 2139 ++++++++++++++++++++
.../artemis/spi/core/security/scram/UserData.java | 53 +
.../core/security/jaas/Krb5LoginModuleTest.java | 13 +-
examples/protocols/amqp/pom.xml | 1 +
examples/protocols/amqp/sasl-scram/pom.xml | 43 +
.../protocols/amqp/sasl-scram/sasl-client/pom.xml | 49 +
.../amqp/sasl-scram/sasl-client/readme.md | 3 +
.../activemq/artemis/jms/example/QPIDClient.java | 48 +
.../protocols/amqp/sasl-scram/sasl-server/pom.xml | 54 +
.../amqp/sasl-scram/sasl-server/readme.md | 3 +
.../activemq/artemis/jms/example/TestServer.java | 35 +
.../src/main/resources/artemis-roles.properties | 18 +
.../src/main/resources/artemis-users.properties | 24 +
.../sasl-server/src/main/resources/broker.xml | 50 +
.../sasl-server/src/main/resources/login.conf | 29 +
tests/integration-tests/.gitignore | 2 +
.../amqp/connect/AMQPConnectSaslTest.java | 170 +-
.../tests/integration/amqp/sasl/SaslScramTest.java | 117 ++
.../test/resources/artemis-scram-roles.properties | 18 +
.../test/resources/artemis-scram-users.properties | 26 +
.../src/test/resources/broker-saslscram.xml | 50 +
.../src/test/resources/login.config | 12 +
47 files changed, 4899 insertions(+), 271 deletions(-)
diff --git a/artemis-protocols/artemis-amqp-protocol/pom.xml b/artemis-protocols/artemis-amqp-protocol/pom.xml
index 1911f25..8fb0e45 100644
--- a/artemis-protocols/artemis-amqp-protocol/pom.xml
+++ b/artemis-protocols/artemis-amqp-protocol/pom.xml
@@ -39,6 +39,11 @@
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
+ <artifactId>artemis-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.activemq</groupId>
<artifactId>artemis-core-client</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java
index a95914f..2068d11 100644
--- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java
@@ -63,10 +63,12 @@ import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContex
import org.apache.activemq.artemis.protocol.amqp.proton.SenderController;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASLFactory;
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry;
import org.apache.activemq.artemis.spi.core.remoting.ClientConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.UUIDGenerator;
import org.apache.qpid.proton.amqp.Symbol;
@@ -96,8 +98,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
private int retryCounter = 0;
private boolean connecting = false;
private volatile ScheduledFuture reconnectFuture;
- private Set<Queue> senders = new HashSet<>();
- private Set<Queue> receivers = new HashSet<>();
+ private final Set<Queue> senders = new HashSet<>();
+ private final Set<Queue> receivers = new HashSet<>();
final Executor connectExecutor;
final ScheduledExecutorService scheduledExecutorService;
@@ -676,7 +678,15 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
if (availableMechanisms.contains(EXTERNAL) && ExternalSASLMechanism.isApplicable(connection)) {
return new ExternalSASLMechanism();
}
-
+ if (SCRAMClientSASL.isApplicable(brokerConnectConfiguration.getUser(),
+ brokerConnectConfiguration.getPassword())) {
+ for (SCRAM scram : SCRAM.values()) {
+ if (availableMechanisms.contains(scram.getName())) {
+ return new SCRAMClientSASL(scram, brokerConnectConfiguration.getUser(),
+ brokerConnectConfiguration.getPassword());
+ }
+ }
+ }
if (availableMechanisms.contains(PLAIN) && PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword())) {
return new PlainSASLMechanism(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword());
}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java
new file mode 100644
index 0000000..2842e90
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java
@@ -0,0 +1,97 @@
+/*
+ * 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.protocol.amqp.sasl.scram;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.UUID;
+
+import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramClientFunctionality.State;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.qpid.proton.codec.DecodeException;
+
+/**
+ * implements the client part of SASL-SCRAM for broker interconnect
+ */
+public class SCRAMClientSASL implements ClientSASL {
+ private final SCRAM scramType;
+ private final ScramClientFunctionalityImpl client;
+ private final String username;
+ private final String password;
+
+ /**
+ * @param scram the SCRAM mechanism to use
+ * @param username the username for authentication
+ * @param password the password for authentication
+ */
+
+ public SCRAMClientSASL(SCRAM scram, String username, String password) {
+ this(scram, username, password, UUID.randomUUID().toString());
+ }
+
+ protected SCRAMClientSASL(SCRAM scram, String username, String password, String nonce) {
+ Objects.requireNonNull(scram);
+ Objects.requireNonNull(username);
+ Objects.requireNonNull(password);
+ this.username = username;
+ this.password = password;
+ this.scramType = scram;
+ client = new ScramClientFunctionalityImpl(scram.getDigest(), scram.getHmac(), nonce);
+ }
+
+ @Override
+ public String getName() {
+ return scramType.getName();
+ }
+
+ @Override
+ public byte[] getInitialResponse() {
+ try {
+ String firstMessage = client.prepareFirstMessage(username);
+ return firstMessage.getBytes(StandardCharsets.US_ASCII);
+ } catch (ScramException e) {
+ throw new DecodeException("prepareFirstMessage failed", e);
+ }
+ }
+
+ @Override
+ public byte[] getResponse(byte[] challenge) {
+ String msg = new String(challenge, StandardCharsets.US_ASCII);
+ if (client.getState() == State.FIRST_PREPARED) {
+ try {
+ String finalMessage = client.prepareFinalMessage(password, msg);
+ return finalMessage.getBytes(StandardCharsets.US_ASCII);
+ } catch (ScramException e) {
+ throw new DecodeException("prepareFinalMessage failed", e);
+ }
+ } else if (client.getState() == State.FINAL_PREPARED) {
+ try {
+ client.checkServerFinalMessage(msg);
+ } catch (ScramException e) {
+ throw new DecodeException("checkServerFinalMessage failed", e);
+ }
+ }
+ return new byte[0];
+ }
+
+ public static boolean isApplicable(String username, String password) {
+ return username != null && username.length() > 0 && password != null && password.length() > 0;
+ }
+
+}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASL.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASL.java
new file mode 100644
index 0000000..2c6826e
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASL.java
@@ -0,0 +1,163 @@
+/*
+ * 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.protocol.amqp.sasl.scram;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
+import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
+import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+
+public abstract class SCRAMServerSASL implements ServerSASL {
+
+ protected final ScramServerFunctionality scram;
+ protected final SCRAM mechanism;
+ private SASLResult result;
+
+ public SCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
+ this(mechanism, UUID.randomUUID().toString());
+ }
+
+ protected SCRAMServerSASL(SCRAM mechanism, String nonce) throws NoSuchAlgorithmException {
+ this.mechanism = mechanism;
+ this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), nonce);
+ }
+
+ @Override
+ public String getName() {
+ return mechanism.getName();
+ }
+
+ @Override
+ public byte[] processSASL(byte[] bytes) {
+ String message = new String(bytes, StandardCharsets.US_ASCII);
+ try {
+ switch (scram.getState()) {
+ case INITIAL: {
+ String userName = scram.handleClientFirstMessage(message);
+ UserData userData = aquireUserData(userName);
+ result = new SCRAMSASLResult(userName, scram, createSaslSubject(userName, userData));
+ String challenge = scram.prepareFirstMessage(userData);
+ return challenge.getBytes(StandardCharsets.US_ASCII);
+ }
+ case PREPARED_FIRST: {
+ String finalMessage = scram.prepareFinalMessage(message);
+ return finalMessage.getBytes(StandardCharsets.US_ASCII);
+ }
+ default:
+ result = new SCRAMFailedSASLResult();
+ break;
+ }
+ } catch (GeneralSecurityException | ScramException | RuntimeException e) {
+ result = new SCRAMFailedSASLResult();
+ failed(e);
+ }
+ return null;
+ }
+
+ protected abstract UserData aquireUserData(String userName) throws LoginException;
+
+ protected abstract void failed(Exception e);
+
+ protected Subject createSaslSubject(String userName, UserData userData) {
+ UserPrincipal userPrincipal = new UserPrincipal(userName);
+ Subject saslSubject = new Subject(true, Collections.singleton(userPrincipal), Collections.singleton(userData),
+ Collections.emptySet());
+ return saslSubject;
+ }
+
+ @Override
+ public SASLResult result() {
+ if (result instanceof SCRAMSASLResult) {
+ return scram.isEnded() ? result : null;
+ }
+ return result;
+ }
+
+ public boolean isEnded() {
+ return scram.isEnded();
+ }
+
+ private static final class SCRAMSASLResult implements SASLResult {
+
+ private final String userName;
+ private final ScramServerFunctionality scram;
+ private final Subject subject;
+
+ SCRAMSASLResult(String userName, ScramServerFunctionality scram, Subject subject) {
+ this.userName = userName;
+ this.scram = scram;
+ this.subject = subject;
+ }
+
+ @Override
+ public String getUser() {
+ return userName;
+ }
+
+ @Override
+ public Subject getSubject() {
+ return subject;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return userName != null && scram.isEnded() && scram.isSuccessful();
+ }
+
+ @Override
+ public String toString() {
+ return "SCRAMSASLResult: userName = " + userName + ", state = " + scram.getState();
+ }
+
+ }
+
+ private static final class SCRAMFailedSASLResult implements SASLResult {
+
+ @Override
+ public String getUser() {
+ return null;
+ }
+
+ @Override
+ public Subject getSubject() {
+ return null;
+ }
+
+ @Override
+ public boolean isSuccess() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "SCRAMFailedSASLResult";
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java
new file mode 100644
index 0000000..67ec428
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java
@@ -0,0 +1,157 @@
+/*
+ * 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.protocol.amqp.sasl.scram;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Iterator;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.activemq.artemis.core.server.ActiveMQServer;
+import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor;
+import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
+import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
+import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory;
+import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager;
+import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
+import org.apache.activemq.artemis.spi.core.remoting.Connection;
+import org.apache.activemq.artemis.spi.core.security.jaas.DigestCallback;
+import org.apache.activemq.artemis.spi.core.security.jaas.HmacCallback;
+import org.apache.activemq.artemis.spi.core.security.jaas.SCRAMMechanismCallback;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+import org.jboss.logging.Logger;
+
+/**
+ * abstract class that implements the SASL-SCRAM authentication scheme, concrete implementations
+ * must supply the {@link SCRAM} type to use and be register via SPI
+ */
+public abstract class SCRAMServerSASLFactory implements ServerSASLFactory {
+
+ private final Logger logger = Logger.getLogger(getClass());
+ private final SCRAM scramType;
+
+ public SCRAMServerSASLFactory(SCRAM scram) {
+ this.scramType = scram;
+ }
+
+ @Override
+ public String getMechanism() {
+ return scramType.getName();
+ }
+
+ @Override
+ public boolean isDefaultPermitted() {
+ return false;
+ }
+
+ @Override
+ public ServerSASL create(ActiveMQServer server, ProtocolManager<AmqpInterceptor> manager, Connection connection,
+ RemotingConnection remotingConnection) {
+ try {
+ if (manager instanceof ProtonProtocolManager) {
+ String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope();
+ return new JAASSCRAMServerSASL(scramType, loginConfigScope, logger);
+ }
+ } catch (NoSuchAlgorithmException e) {
+ // can't be used then...
+ }
+ return null;
+ }
+
+ private static final class JAASSCRAMServerSASL extends SCRAMServerSASL {
+
+ private final String loginConfigScope;
+ private LoginContext loginContext = null;
+ private Subject loginSubject;
+ private final Logger logger;
+
+ JAASSCRAMServerSASL(SCRAM scram, String loginConfigScope, Logger logger) throws NoSuchAlgorithmException {
+ super(scram);
+ this.loginConfigScope = loginConfigScope;
+ this.logger = logger;
+ }
+
+ @Override
+ protected UserData aquireUserData(String userName) throws LoginException {
+ loginContext = new LoginContext(loginConfigScope, new CallbackHandler() {
+
+ @Override
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback) {
+ ((NameCallback) callback).setName(userName);
+ } else if (callback instanceof SCRAMMechanismCallback) {
+ ((SCRAMMechanismCallback) callback).setMechanism(mechanism.getName());
+ } else if (callback instanceof DigestCallback) {
+ ((DigestCallback) callback).setDigest(scram.getDigest());
+ } else if (callback instanceof HmacCallback) {
+ ((HmacCallback) callback).setHmac(scram.getHmac());
+ } else {
+ throw new UnsupportedCallbackException(callback, "Unrecognized Callback " +
+ callback.getClass().getSimpleName());
+ }
+ }
+ }
+ });
+ loginContext.login();
+ loginSubject = loginContext.getSubject();
+ Iterator<UserData> credentials = loginSubject.getPublicCredentials(UserData.class).iterator();
+ if (credentials.hasNext()) {
+ return credentials.next();
+ }
+ throw new LoginException("can't aquire user data through configured login config scope (" + loginConfigScope +
+ ")");
+ }
+
+ @Override
+ protected Subject createSaslSubject(String userName, UserData userData) {
+ if (loginSubject != null) {
+ return new Subject(true, loginSubject.getPrincipals(), loginSubject.getPublicCredentials(),
+ loginSubject.getPrivateCredentials());
+ }
+ return super.createSaslSubject(userName, userData);
+ }
+
+ @Override
+ public void done() {
+ if (loginContext != null) {
+ try {
+ loginContext.logout();
+ } catch (LoginException e1) {
+ // we can't do anything useful then...
+ }
+ }
+ loginContext = null;
+ loginSubject = null;
+ }
+
+ @Override
+ protected void failed(Exception e) {
+ logger.warn("SASL-SCRAM Authentication failed", e);
+ }
+
+ }
+
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java
similarity index 57%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java
index 9306d5f..f7a1308 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java
@@ -14,33 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.activemq.artemis.spi.core.security.jaas;
+package org.apache.activemq.artemis.protocol.amqp.sasl.scram;
-import javax.security.auth.callback.Callback;
-import java.security.Principal;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
/**
- * A Callback for kerberos peer principal.
+ * provides SASL SRAM-SHA256
*/
-public class Krb5Callback implements Callback {
+public class SHA256SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
- Principal peerPrincipal;
-
- /**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
- */
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public SHA256SCRAMServerSASLFactory() {
+ super(SCRAM.SHA256);
}
- /**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
- */
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ @Override
+ public int getPrecedence() {
+ return 256;
}
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java
similarity index 57%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java
index 9306d5f..93c4008 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java
@@ -14,33 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.activemq.artemis.spi.core.security.jaas;
+package org.apache.activemq.artemis.protocol.amqp.sasl.scram;
-import javax.security.auth.callback.Callback;
-import java.security.Principal;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
/**
- * A Callback for kerberos peer principal.
+ * provides SASL SRAM-SHA512
*/
-public class Krb5Callback implements Callback {
+public class SHA512SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
- Principal peerPrincipal;
-
- /**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
- */
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public SHA512SCRAMServerSASLFactory() {
+ super(SCRAM.SHA512);
}
- /**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
- */
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ @Override
+ public int getPrecedence() {
+ return 512;
}
}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionality.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionality.java
new file mode 100644
index 0000000..6a243f3
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionality.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.protocol.amqp.sasl.scram;
+
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+
+/**
+ * Provides building blocks for creating SCRAM authentication client
+ */
+@SuppressWarnings("unused")
+public interface ScramClientFunctionality {
+ /**
+ * Prepares the first client message
+ * @param username Username of the user
+ * @return First client message
+ * @throws ScramException if username contains prohibited characters
+ */
+ String prepareFirstMessage(String username) throws ScramException;
+
+ /**
+ * Prepares client's final message
+ * @param password User password
+ * @param serverFirstMessage Server's first message
+ * @return Client's final message
+ * @throws ScramException if there is an error processing server's message, i.e. it violates the
+ * protocol
+ */
+ String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException;
+
+ /**
+ * Checks if the server's final message is valid
+ * @param serverFinalMessage Server's final message
+ * @throws ScramException if there is an error processing server's message, i.e. it violates the
+ * protocol
+ */
+ void checkServerFinalMessage(String serverFinalMessage) throws ScramException;
+
+ /**
+ * Checks if authentication is successful. You can call this method only if authentication is
+ * completed. Ensure that using {@link #isEnded()}
+ * @return true if successful, false otherwise
+ */
+ boolean isSuccessful();
+
+ /**
+ * Checks if authentication is completed, either successfully or not. Authentication is completed
+ * if {@link #getState()} returns ENDED.
+ * @return true if authentication has ended
+ */
+ boolean isEnded();
+
+ /**
+ * Gets the state of the authentication procedure
+ * @return Current state
+ */
+ State getState();
+
+ /**
+ * State of the authentication procedure
+ */
+ enum State {
+ /**
+ * Initial state
+ */
+ INITIAL,
+ /**
+ * State after first message is prepared
+ */
+ FIRST_PREPARED,
+ /**
+ * State after final message is prepared
+ */
+ FINAL_PREPARED,
+ /**
+ * Authentication is completes, either successfully or not
+ */
+ ENDED
+ }
+}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionalityImpl.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionalityImpl.java
new file mode 100644
index 0000000..1f566e6
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionalityImpl.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.protocol.amqp.sasl.scram;
+
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.Mac;
+
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
+import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
+
+/**
+ * Provides building blocks for creating SCRAM authentication client
+ */
+@SuppressWarnings("unused")
+public class ScramClientFunctionalityImpl implements ScramClientFunctionality {
+ private static final Pattern SERVER_FIRST_MESSAGE = Pattern.compile("r=([^,]*),s=([^,]*),i=(.*)$");
+ private static final Pattern SERVER_FINAL_MESSAGE = Pattern.compile("v=([^,]*)$");
+
+ private static final String GS2_HEADER = "n,,";
+ private static final Charset ASCII = Charset.forName("ASCII");
+
+ private final String mDigestName;
+ private final String mHmacName;
+ private final String mClientNonce;
+ private String mClientFirstMessageBare;
+
+ private final boolean mIsSuccessful = false;
+ private byte[] mSaltedPassword;
+ private String mAuthMessage;
+
+ private State mState = State.INITIAL;
+
+ /**
+ * Create new ScramClientFunctionalityImpl
+ * @param digestName Digest to be used
+ * @param hmacName HMAC to be used
+ */
+ public ScramClientFunctionalityImpl(String digestName, String hmacName) {
+ this(digestName, hmacName, UUID.randomUUID().toString());
+ }
+
+ /**
+ * Create new ScramClientFunctionalityImpl
+ * @param digestName Digest to be used
+ * @param hmacName HMAC to be used
+ * @param clientNonce Client nonce to be used
+ */
+ public ScramClientFunctionalityImpl(String digestName, String hmacName, String clientNonce) {
+ if (ScramUtils.isNullOrEmpty(digestName)) {
+ throw new NullPointerException("digestName cannot be null or empty");
+ }
+ if (ScramUtils.isNullOrEmpty(hmacName)) {
+ throw new NullPointerException("hmacName cannot be null or empty");
+ }
+ if (ScramUtils.isNullOrEmpty(clientNonce)) {
+ throw new NullPointerException("clientNonce cannot be null or empty");
+ }
+
+ mDigestName = digestName;
+ mHmacName = hmacName;
+ mClientNonce = clientNonce;
+ }
+
+ /**
+ * Prepares first client message You may want to use
+ * {@link StringPrep#isContainingProhibitedCharacters(String)} in order to check if the username
+ * contains only valid characters
+ * @param username Username
+ * @return prepared first message
+ * @throws ScramException if <code>username</code> contains prohibited characters
+ */
+ @Override
+ public String prepareFirstMessage(String username) throws ScramException {
+ if (mState != State.INITIAL) {
+ throw new IllegalStateException("You can call this method only once");
+ }
+
+ try {
+ mClientFirstMessageBare = "n=" + StringPrep.prepAsQueryString(username) + ",r=" + mClientNonce;
+ mState = State.FIRST_PREPARED;
+ return GS2_HEADER + mClientFirstMessageBare;
+ } catch (StringPrep.StringPrepError e) {
+ mState = State.ENDED;
+ throw new ScramException("Username contains prohibited character");
+ }
+ }
+
+ @Override
+ public String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException {
+ if (mState != State.FIRST_PREPARED) {
+ throw new IllegalStateException("You can call this method once only after " + "calling prepareFirstMessage()");
+ }
+
+ Matcher m = SERVER_FIRST_MESSAGE.matcher(serverFirstMessage);
+ if (!m.matches()) {
+ mState = State.ENDED;
+ return null;
+ }
+
+ String nonce = m.group(1);
+
+ if (!nonce.startsWith(mClientNonce)) {
+ mState = State.ENDED;
+ return null;
+ }
+
+ String salt = m.group(2);
+ String iterationCountString = m.group(3);
+ int iterations = Integer.parseInt(iterationCountString);
+ if (iterations <= 0) {
+ mState = State.ENDED;
+ return null;
+ }
+
+ try {
+ mSaltedPassword = ScramUtils.generateSaltedPassword(password, Base64.getDecoder().decode(salt), iterations,
+ Mac.getInstance(mHmacName));
+
+ String clientFinalMessageWithoutProof =
+ "c=" + Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII)) + ",r=" + nonce;
+
+ mAuthMessage = mClientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
+
+ byte[] clientKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Client Key");
+ byte[] storedKey = MessageDigest.getInstance(mDigestName).digest(clientKey);
+
+ byte[] clientSignature = ScramUtils.computeHmac(storedKey, mHmacName, mAuthMessage);
+
+ byte[] clientProof = clientKey.clone();
+ for (int i = 0; i < clientProof.length; i++) {
+ clientProof[i] ^= clientSignature[i];
+ }
+
+ mState = State.FINAL_PREPARED;
+ return clientFinalMessageWithoutProof + ",p=" + Base64.getEncoder().encodeToString(clientProof);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ mState = State.ENDED;
+ throw new ScramException(e);
+ }
+ }
+
+ @Override
+ public void checkServerFinalMessage(String serverFinalMessage) throws ScramException {
+ if (mState != State.FINAL_PREPARED) {
+ throw new IllegalStateException("You can call this method only once after " + "calling prepareFinalMessage()");
+ }
+
+ Matcher m = SERVER_FINAL_MESSAGE.matcher(serverFinalMessage);
+ if (!m.matches()) {
+ mState = State.ENDED;
+ throw new ScramException("invalid message format");
+ }
+
+ byte[] serverSignature = Base64.getDecoder().decode(m.group(1));
+
+ mState = State.ENDED;
+ if (!Arrays.equals(serverSignature, getExpectedServerSignature())) {
+ throw new ScramException("Server signature missmatch");
+ }
+ }
+
+ @Override
+ public boolean isSuccessful() {
+ if (mState == State.ENDED) {
+ return mIsSuccessful;
+ } else {
+ throw new IllegalStateException("You cannot call this method before authentication is ended. " +
+ "Use isEnded() to check that");
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return mState == State.ENDED;
+ }
+
+ @Override
+ public State getState() {
+ return mState;
+ }
+
+ private byte[] getExpectedServerSignature() throws ScramException {
+ try {
+ byte[] serverKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Server Key");
+ return ScramUtils.computeHmac(serverKey, mHmacName, mAuthMessage);
+ } catch (InvalidKeyException | NoSuchAlgorithmException e) {
+ mState = State.ENDED;
+ throw new ScramException(e);
+ }
+ }
+}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionality.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionality.java
new file mode 100644
index 0000000..b78575c
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionality.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.protocol.amqp.sasl.scram;
+
+import java.security.MessageDigest;
+
+import javax.crypto.Mac;
+
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+
+/**
+ * Provides building blocks for creating SCRAM authentication server
+ */
+public interface ScramServerFunctionality {
+ /**
+ * Handles client's first message
+ * @param message Client's first message
+ * @return username extracted from the client message
+ * @throws ScramException
+ */
+ String handleClientFirstMessage(String message) throws ScramException;
+
+ /**
+ * Prepares server's first message
+ * @param userData user data needed to prepare the message
+ * @return Server's first message
+ */
+ String prepareFirstMessage(UserData userData);
+
+ /**
+ * Prepares server's final message
+ * @param clientFinalMessage Client's final message
+ * @return Server's final message
+ * @throws ScramException
+ */
+ String prepareFinalMessage(String clientFinalMessage) throws ScramException;
+
+ /**
+ * Checks if authentication is completed, either successfully or not. Authentication is completed
+ * if {@link #getState()} returns ENDED.
+ * @return true if authentication has ended
+ */
+ boolean isSuccessful();
+
+ /**
+ * Checks if authentication is completed, either successfully or not. Authentication is completed
+ * if {@link #getState()} returns ENDED.
+ * @return true if authentication has ended
+ */
+ boolean isEnded();
+
+ /**
+ * Gets the state of the authentication procedure
+ * @return Current state
+ */
+ State getState();
+
+ /**
+ * State of the authentication procedure
+ */
+ enum State {
+ /**
+ * Initial state
+ */
+ INITIAL,
+ /**
+ * First client message is handled (username is extracted)
+ */
+ FIRST_CLIENT_MESSAGE_HANDLED,
+ /**
+ * First server message is prepared
+ */
+ PREPARED_FIRST,
+ /**
+ * Authentication is completes, either successfully or not
+ */
+ ENDED
+ }
+
+ MessageDigest getDigest();
+
+ Mac getHmac();
+}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionalityImpl.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionalityImpl.java
new file mode 100644
index 0000000..07cc0e1
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionalityImpl.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.protocol.amqp.sasl.scram;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.Mac;
+
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+
+/**
+ * Provides building blocks for creating SCRAM authentication server
+ */
+public class ScramServerFunctionalityImpl implements ScramServerFunctionality {
+ private static final Pattern CLIENT_FIRST_MESSAGE =
+ Pattern.compile("^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$");
+ private static final Pattern CLIENT_FINAL_MESSAGE = Pattern.compile("(c=([^,]*),r=([^,]*)),p=(.*)$");
+
+ private final String mServerPartNonce;
+
+ private boolean mIsSuccessful = false;
+ private State mState = State.INITIAL;
+ private String mClientFirstMessageBare;
+ private String mNonce;
+ private String mServerFirstMessage;
+ private UserData mUserData;
+ private final MessageDigest digest;
+ private final Mac hmac;
+
+ /**
+ * Creates new ScramServerFunctionalityImpl
+ * @param digestName Digest to be used
+ * @param hmacName HMAC to be used
+ * @throws NoSuchAlgorithmException
+ */
+ public ScramServerFunctionalityImpl(String digestName, String hmacName) throws NoSuchAlgorithmException {
+ this(digestName, hmacName, UUID.randomUUID().toString());
+ }
+
+ /**
+ * /** Creates new ScramServerFunctionalityImpl
+ * @param digestName Digest to be used
+ * @param hmacName HMAC to be used
+ * @param serverPartNonce Server's part of the nonce
+ * @throws NoSuchAlgorithmException
+ */
+ public ScramServerFunctionalityImpl(String digestName, String hmacName,
+ String serverPartNonce) throws NoSuchAlgorithmException {
+ if (ScramUtils.isNullOrEmpty(digestName)) {
+ throw new NullPointerException("digestName cannot be null or empty");
+ }
+ if (ScramUtils.isNullOrEmpty(hmacName)) {
+ throw new NullPointerException("hmacName cannot be null or empty");
+ }
+ if (ScramUtils.isNullOrEmpty(serverPartNonce)) {
+ throw new NullPointerException("serverPartNonce cannot be null or empty");
+ }
+ digest = MessageDigest.getInstance(digestName);
+ hmac = Mac.getInstance(hmacName);
+ mServerPartNonce = serverPartNonce;
+ }
+
+ /**
+ * Handles client's first message
+ * @param message Client's first message
+ * @return username extracted from the client message
+ * @throws ScramException
+ */
+ @Override
+ public String handleClientFirstMessage(String message) throws ScramException {
+ Matcher m = CLIENT_FIRST_MESSAGE.matcher(message);
+ if (!m.matches()) {
+ mState = State.ENDED;
+ throw new ScramException("Invalid message received");
+ }
+
+ mClientFirstMessageBare = m.group(5);
+ String username = m.group(6);
+ String clientNonce = m.group(7);
+ mNonce = clientNonce + mServerPartNonce;
+
+ mState = State.FIRST_CLIENT_MESSAGE_HANDLED;
+
+ return username;
+ }
+
+ @Override
+ public String prepareFirstMessage(UserData userData) {
+ mUserData = userData;
+ mState = State.PREPARED_FIRST;
+ mServerFirstMessage = String.format("r=%s,s=%s,i=%d", mNonce, userData.salt, userData.iterations);
+
+ return mServerFirstMessage;
+ }
+
+ @Override
+ public String prepareFinalMessage(String clientFinalMessage) throws ScramException {
+ String finalMessage = prepareFinalMessageUnchecked(clientFinalMessage);
+ if (!mIsSuccessful) {
+ throw new ScramException("client credentials missmatch");
+ }
+ return finalMessage;
+ }
+
+ public String prepareFinalMessageUnchecked(String clientFinalMessage) throws ScramException {
+ mState = State.ENDED;
+ Matcher m = CLIENT_FINAL_MESSAGE.matcher(clientFinalMessage);
+ if (!m.matches()) {
+ throw new ScramException("Invalid message received");
+ }
+
+ String clientFinalMessageWithoutProof = m.group(1);
+ String clientNonce = m.group(3);
+ String proof = m.group(4);
+
+ if (!mNonce.equals(clientNonce)) {
+ throw new ScramException("Nonce mismatch");
+ }
+
+ String authMessage = mClientFirstMessageBare + "," + mServerFirstMessage + "," + clientFinalMessageWithoutProof;
+
+ byte[] storedKeyArr = Base64.getDecoder().decode(mUserData.storedKey);
+ byte[] clientSignature = ScramUtils.computeHmac(storedKeyArr, hmac, authMessage);
+ byte[] serverSignature =
+ ScramUtils.computeHmac(Base64.getDecoder().decode(mUserData.serverKey), hmac, authMessage);
+ byte[] clientKey = clientSignature.clone();
+ byte[] decodedProof = Base64.getDecoder().decode(proof);
+ for (int i = 0; i < clientKey.length; i++) {
+ clientKey[i] ^= decodedProof[i];
+ }
+
+ byte[] resultKey = digest.digest(clientKey);
+ mIsSuccessful = Arrays.equals(storedKeyArr, resultKey);
+ return "v=" + Base64.getEncoder().encodeToString(serverSignature);
+ }
+
+ @Override
+ public boolean isSuccessful() {
+ if (mState == State.ENDED) {
+ return mIsSuccessful;
+ } else {
+ throw new IllegalStateException("You cannot call this method before authentication is ended. " +
+ "Use isEnded() to check that");
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return mState == State.ENDED;
+ }
+
+ @Override
+ public State getState() {
+ return mState;
+ }
+
+ @Override
+ public MessageDigest getDigest() {
+ try {
+ return (MessageDigest) digest.clone();
+ } catch (CloneNotSupportedException cns) {
+ try {
+ return MessageDigest.getInstance(digest.getAlgorithm());
+ } catch (NoSuchAlgorithmException nsa) {
+ throw new AssertionError(nsa);
+ }
+ }
+ }
+
+ @Override
+ public Mac getHmac() {
+ try {
+ return (Mac) hmac.clone();
+ } catch (CloneNotSupportedException cns) {
+ try {
+ return Mac.getInstance(hmac.getAlgorithm());
+ } catch (NoSuchAlgorithmException nsa) {
+ throw new AssertionError(nsa);
+ }
+ }
+ }
+}
diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory b/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory
index eb2c112..c8f54d2 100644
--- a/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory
+++ b/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory
@@ -1,4 +1,6 @@
org.apache.activemq.artemis.protocol.amqp.sasl.AnonymousServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.PlainServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASLFactory
-org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
\ No newline at end of file
+org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
+org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory
+org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512SCRAMServerSASLFactory
\ No newline at end of file
diff --git a/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/SCRAMTest.java b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/SCRAMTest.java
new file mode 100644
index 0000000..f646d6e
--- /dev/null
+++ b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/SCRAMTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.protocol.amqp.sasl;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.crypto.Mac;
+import javax.security.auth.login.LoginException;
+
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramServerFunctionalityImpl;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+import org.apache.qpid.proton.codec.DecodeException;
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * test cases for the SASL-SCRAM
+ */
+@RunWith(Parameterized.class)
+public class SCRAMTest {
+
+ /**
+ *
+ */
+ private final SCRAM mechanism;
+ private static final byte[] SALT = new byte[32];
+ private static final String SNONCE = "server";
+ private static final String CNONCE = "client";
+ private static final String USERNAME = "test";
+ private static final String PASSWORD = "123";
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> data() {
+ List<Object[]> list = new ArrayList<>();
+ for (SCRAM scram : SCRAM.values()) {
+ list.add(new Object[] {scram});
+ }
+ return list;
+ }
+
+ public SCRAMTest(SCRAM mechanism) {
+ this.mechanism = mechanism;
+ }
+
+ @Test
+ public void testSuccess() throws NoSuchAlgorithmException {
+ TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
+ TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
+ byte[] clientFirst = clientSASL.getInitialResponse();
+ assertNotNull(clientFirst);
+ byte[] serverFirst = serverSASL.processSASL(clientFirst);
+ assertNotNull(serverFirst);
+ assertNull(serverSASL.result());
+ byte[] clientFinal = clientSASL.getResponse(serverFirst);
+ assertNotNull(clientFinal);
+ assertFalse(clientFinal.length == 0);
+ byte[] serverFinal = serverSASL.processSASL(clientFinal);
+ assertNotNull(serverFinal);
+ assertNotNull(serverSASL.result());
+ assertNotNull(serverSASL.result().getSubject());
+ assertEquals(USERNAME, serverSASL.result().getUser());
+ assertNull(serverSASL.exception);
+ assertTrue(serverSASL.result().isSuccess());
+ byte[] clientCheck = clientSASL.getResponse(serverFinal);
+ assertNotNull(clientCheck);
+ assertTrue(clientCheck.length == 0);
+ }
+
+ @Test
+ public void testWrongClientPassword() throws NoSuchAlgorithmException {
+ TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
+ TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, "xyz");
+ byte[] clientFirst = clientSASL.getInitialResponse();
+ assertNotNull(clientFirst);
+ byte[] serverFirst = serverSASL.processSASL(clientFirst);
+ assertNotNull(serverFirst);
+ assertNull(serverSASL.result());
+ byte[] clientFinal = clientSASL.getResponse(serverFirst);
+ assertNotNull(clientFinal);
+ assertFalse(clientFinal.length == 0);
+ byte[] serverFinal = serverSASL.processSASL(clientFinal);
+ assertNull(serverFinal);
+ assertNotNull(serverSASL.result());
+ assertFalse(serverSASL.result().isSuccess());
+ assertThat(serverSASL.exception, IsInstanceOf.instanceOf(ScramException.class));
+ }
+
+ @Test(expected = DecodeException.class)
+ public void testServerTryTrickClient() throws NoSuchAlgorithmException, ScramException {
+ TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
+ ScramServerFunctionalityImpl bad =
+ new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), SNONCE);
+ byte[] clientFirst = clientSASL.getInitialResponse();
+ assertNotNull(clientFirst);
+ bad.handleClientFirstMessage(new String(clientFirst, StandardCharsets.US_ASCII));
+ byte[] serverFirst =
+ bad.prepareFirstMessage(generateUserData(mechanism, "bad")).getBytes(StandardCharsets.US_ASCII);
+ byte[] clientFinal = clientSASL.getResponse(serverFirst);
+ assertNotNull(clientFinal);
+ assertFalse(clientFinal.length == 0);
+ byte[] serverFinal = bad.prepareFinalMessageUnchecked(new String(clientFinal, StandardCharsets.US_ASCII))
+ .getBytes(StandardCharsets.US_ASCII);
+ clientSASL.getResponse(serverFinal);
+ }
+
+ private static UserData generateUserData(SCRAM mechanism, String password) throws NoSuchAlgorithmException,
+ ScramException {
+ MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
+ Mac hmac = Mac.getInstance(mechanism.getHmac());
+ ScramUtils.NewPasswordStringData data =
+ ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, SALT, 4096, digest, hmac));
+ return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
+ }
+
+
+ private static final class TestSCRAMClientSASL extends SCRAMClientSASL {
+
+ TestSCRAMClientSASL(SCRAM scram, String username, String password) {
+ super(scram, username, password, CNONCE);
+ }
+
+ }
+
+ private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
+
+ private Exception exception;
+ private final String username;
+ private final String password;
+
+ TestSCRAMServerSASL(SCRAM mechanism, String username, String password) throws NoSuchAlgorithmException {
+ super(mechanism, SNONCE);
+ this.username = username;
+ this.password = password;
+ }
+
+ @Override
+ public void done() {
+ // nothing to do
+ }
+
+ @Override
+ protected UserData aquireUserData(String userName) throws LoginException {
+ if (!this.username.equals(userName)) {
+ throw new LoginException("invalid username");
+ }
+ try {
+ return generateUserData(mechanism, password);
+ } catch (Exception e) {
+ throw new LoginException(e.getMessage());
+ }
+ }
+
+ @Override
+ protected void failed(Exception e) {
+ this.exception = e;
+ }
+
+ }
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java
similarity index 67%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java
copy to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java
index 1047e08..d9c7447 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java
@@ -16,36 +16,35 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import org.jboss.logging.Logger;
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
-import java.io.IOException;
-import java.security.Principal;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+
+import org.jboss.logging.Logger;
/**
- * populate a subject with kerberos credential from the handler
+ * Abstract login module that uses an external authenticated principal
*/
-public class Krb5LoginModule implements AuditLoginModule {
-
- private static final Logger logger = Logger.getLogger(Krb5LoginModule.class);
+public abstract class AbstractPrincipalLoginModule implements AuditLoginModule {
+ private final Logger logger = Logger.getLogger(getClass());
private Subject subject;
- private final List<Principal> principals = new LinkedList<>();
+ private final List<Principal> authenticatedPrincipals = new LinkedList<>();
private CallbackHandler callbackHandler;
private boolean loginSucceeded;
- private Principal principal;
+ private Principal[] principals;
@Override
- public void initialize(Subject subject,
- CallbackHandler callbackHandler,
- Map<String, ?> sharedState,
+ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
@@ -55,22 +54,22 @@ public class Krb5LoginModule implements AuditLoginModule {
public boolean login() throws LoginException {
Callback[] callbacks = new Callback[1];
- callbacks[0] = new Krb5Callback();
+ callbacks[0] = new PrincipalsCallback();
try {
callbackHandler.handle(callbacks);
- principal = ((Krb5Callback)callbacks[0]).getPeerPrincipal();
- if (principal != null) {
- principals.add(principal);
+ principals = ((PrincipalsCallback) callbacks[0]).getPeerPrincipals();
+ if (principals != null) {
+ authenticatedPrincipals.addAll(Arrays.asList(principals));
}
} catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
}
- if (!principals.isEmpty()) {
+ if (!authenticatedPrincipals.isEmpty()) {
loginSucceeded = true;
}
- logger.debug("login " + principals);
+ logger.debug("login " + authenticatedPrincipals);
return loginSucceeded;
}
@@ -78,8 +77,8 @@ public class Krb5LoginModule implements AuditLoginModule {
public boolean commit() throws LoginException {
boolean result = loginSucceeded;
if (result) {
- principals.add(new UserPrincipal(principals.get(0).getName()));
- subject.getPrincipals().addAll(principals);
+ authenticatedPrincipals.add(new UserPrincipal(authenticatedPrincipals.get(0).getName()));
+ subject.getPrincipals().addAll(authenticatedPrincipals);
}
clear();
@@ -91,7 +90,11 @@ public class Krb5LoginModule implements AuditLoginModule {
@Override
public boolean abort() throws LoginException {
- registerFailureForAudit(principal != null ? principal.getName() : null);
+ if (principals != null) {
+ for (Principal principal : authenticatedPrincipals) {
+ registerFailureForAudit(principal.getName());
+ }
+ }
clear();
logger.debug("abort");
@@ -100,14 +103,14 @@ public class Krb5LoginModule implements AuditLoginModule {
}
private void clear() {
- principal = null;
+ principals = null;
loginSucceeded = false;
}
@Override
public boolean logout() throws LoginException {
- subject.getPrincipals().removeAll(principals);
- principals.clear();
+ subject.getPrincipals().removeAll(authenticatedPrincipals);
+ authenticatedPrincipals.clear();
clear();
logger.debug("logout");
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java
similarity index 66%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java
index 9306d5f..865b266 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java
@@ -16,31 +16,30 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
+import java.security.MessageDigest;
+
import javax.security.auth.callback.Callback;
-import java.security.Principal;
/**
- * A Callback for kerberos peer principal.
+ * Callback to obtain a {@link MessageDigest} for login purpose
*/
-public class Krb5Callback implements Callback {
+public class DigestCallback implements Callback {
- Principal peerPrincipal;
+ private MessageDigest digest;
/**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
+ * set the digest to use
+ * @param digest the digest
*/
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public void setDigest(MessageDigest digest) {
+ this.digest = digest;
}
/**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
+ * @return the digest or <code>null</code> if not known
*/
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ public MessageDigest getDigest() {
+ return digest;
}
+
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java
similarity index 66%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java
index 9306d5f..8d8f9ce 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java
@@ -16,31 +16,29 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
+import javax.crypto.Mac;
import javax.security.auth.callback.Callback;
-import java.security.Principal;
/**
- * A Callback for kerberos peer principal.
+ * Callback for obtaining information about a used H{@link Mac}
*/
-public class Krb5Callback implements Callback {
+public class HmacCallback implements Callback {
- Principal peerPrincipal;
+ private Mac hmac;
/**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
+ * set the Hmac to use
+ * @param hmac
*/
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public void setHmac(Mac hmac) {
+ this.hmac = hmac;
}
/**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
+ * @return the Hmac or <code>null</code> if non could be obtained
*/
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ public Mac getHmac() {
+ return hmac;
}
+
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCallbackHandler.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCallbackHandler.java
index a765f45..e0c6989 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCallbackHandler.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/JaasCallbackHandler.java
@@ -16,7 +16,12 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
+import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
+import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
@@ -25,11 +30,8 @@ import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.kerberos.KerberosPrincipal;
-import java.io.IOException;
-import java.security.Principal;
-import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
-import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
+import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
/**
* A JAAS username password CallbackHandler.
@@ -67,18 +69,26 @@ public class JaasCallbackHandler implements CallbackHandler {
CertificateCallback certCallback = (CertificateCallback) callback;
certCallback.setCertificates(getCertsFromConnection(remotingConnection));
- } else if (callback instanceof Krb5Callback) {
- Krb5Callback krb5Callback = (Krb5Callback) callback;
+ } else if (callback instanceof PrincipalsCallback) {
+ PrincipalsCallback principalsCallback = (PrincipalsCallback) callback;
Subject peerSubject = remotingConnection.getSubject();
if (peerSubject != null) {
- for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
- krb5Callback.setPeerPrincipal(principal);
+ for (KerberosPrincipal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
+ principalsCallback.setPeerPrincipals(new Principal[] {principal});
+ return;
+ }
+ Set<Principal> principals = peerSubject.getPrincipals();
+ if (principals.size() > 0) {
+ principalsCallback.setPeerPrincipals(principals.toArray(new Principal[0]));
return;
}
}
- krb5Callback.setPeerPrincipal(getPeerPrincipalFromConnection(remotingConnection));
+ Principal peerPrincipalFromConnection = getPeerPrincipalFromConnection(remotingConnection);
+ if (peerPrincipalFromConnection != null) {
+ principalsCallback.setPeerPrincipals(new Principal[] {peerPrincipalFromConnection});
+ }
} else {
throw new UnsupportedCallbackException(callback);
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java
index 1047e08..a3cecb1 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModule.java
@@ -16,102 +16,10 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import org.jboss.logging.Logger;
-
-import javax.security.auth.Subject;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.UnsupportedCallbackException;
-import javax.security.auth.login.LoginException;
-import java.io.IOException;
-import java.security.Principal;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
/**
* populate a subject with kerberos credential from the handler
*/
-public class Krb5LoginModule implements AuditLoginModule {
-
- private static final Logger logger = Logger.getLogger(Krb5LoginModule.class);
-
- private Subject subject;
- private final List<Principal> principals = new LinkedList<>();
- private CallbackHandler callbackHandler;
- private boolean loginSucceeded;
- private Principal principal;
-
- @Override
- public void initialize(Subject subject,
- CallbackHandler callbackHandler,
- Map<String, ?> sharedState,
- Map<String, ?> options) {
- this.subject = subject;
- this.callbackHandler = callbackHandler;
- }
-
- @Override
- public boolean login() throws LoginException {
- Callback[] callbacks = new Callback[1];
-
- callbacks[0] = new Krb5Callback();
- try {
- callbackHandler.handle(callbacks);
- principal = ((Krb5Callback)callbacks[0]).getPeerPrincipal();
- if (principal != null) {
- principals.add(principal);
- }
- } catch (IOException ioe) {
- throw new LoginException(ioe.getMessage());
- } catch (UnsupportedCallbackException uce) {
- throw new LoginException(uce.getMessage() + " not available to obtain information from user");
- }
- if (!principals.isEmpty()) {
- loginSucceeded = true;
- }
- logger.debug("login " + principals);
- return loginSucceeded;
- }
-
- @Override
- public boolean commit() throws LoginException {
- boolean result = loginSucceeded;
- if (result) {
- principals.add(new UserPrincipal(principals.get(0).getName()));
- subject.getPrincipals().addAll(principals);
- }
-
- clear();
-
- logger.debug("commit, result: " + result);
-
- return result;
- }
-
- @Override
- public boolean abort() throws LoginException {
- registerFailureForAudit(principal != null ? principal.getName() : null);
- clear();
-
- logger.debug("abort");
-
- return true;
- }
-
- private void clear() {
- principal = null;
- loginSucceeded = false;
- }
-
- @Override
- public boolean logout() throws LoginException {
- subject.getPrincipals().removeAll(principals);
- principals.clear();
- clear();
+public class Krb5LoginModule extends AbstractPrincipalLoginModule {
- logger.debug("logout");
- return true;
- }
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java
similarity index 74%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java
index 9306d5f..821f6fb 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java
@@ -16,31 +16,30 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import javax.security.auth.callback.Callback;
import java.security.Principal;
+import javax.security.auth.callback.Callback;
+
/**
- * A Callback for kerberos peer principal.
+ * A Callback for getting the peer principals.
*/
-public class Krb5Callback implements Callback {
+public class PrincipalsCallback implements Callback {
- Principal peerPrincipal;
+ Principal[] peerPrincipals;
/**
- * Setter for peer Principal.
- *
+ * Setter for peer Principals.
* @param principal The certificates to be returned.
*/
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public void setPeerPrincipals(Principal[] principal) {
+ peerPrincipals = principal;
}
/**
- * Getter for peer Principal.
- *
+ * Getter for peer Principals.
* @return The principal being carried.
*/
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ public Principal[] getPeerPrincipals() {
+ return peerPrincipals;
}
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java
similarity index 60%
copy from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
copy to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java
index 9306d5f..4da5200 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java
@@ -16,31 +16,10 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import javax.security.auth.callback.Callback;
-import java.security.Principal;
-
/**
- * A Callback for kerberos peer principal.
+ * Handles the actual login after channel authentication has succeed
*/
-public class Krb5Callback implements Callback {
-
- Principal peerPrincipal;
+public class SCRAMLoginModule extends AbstractPrincipalLoginModule {
- /**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
- */
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
- }
- /**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
- */
- public Principal getPeerPrincipal() {
- return peerPrincipal;
- }
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java
similarity index 66%
rename from artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
rename to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java
index 9306d5f..de66108 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5Callback.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java
@@ -17,30 +17,28 @@
package org.apache.activemq.artemis.spi.core.security.jaas;
import javax.security.auth.callback.Callback;
-import java.security.Principal;
/**
- * A Callback for kerberos peer principal.
+ * callback to obtain the a mechanism used in a SASL-SCRAM authentication
*/
-public class Krb5Callback implements Callback {
+public class SCRAMMechanismCallback implements Callback {
- Principal peerPrincipal;
+ private String name;
/**
- * Setter for peer Principal.
- *
- * @param principal The certificates to be returned.
+ * sets the name of the mechanism
+ * @param name the name of the mechanism
*/
- public void setPeerPrincipal(Principal principal) {
- peerPrincipal = principal;
+ public void setMechanism(String name) {
+ this.name = name;
+
}
/**
- * Getter for peer Principal.
- *
- * @return The principal being carried.
+ * @return the name of the mechanism
*/
- public Principal getPeerPrincipal() {
- return peerPrincipal;
+ public String getMechanism() {
+ return name;
}
+
}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java
new file mode 100644
index 0000000..7f52f58
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java
@@ -0,0 +1,237 @@
+/*
+ * 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.spi.core.security.jaas;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.Principal;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.crypto.Mac;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
+import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
+import org.apache.activemq.artemis.spi.core.security.scram.StringPrep.StringPrepError;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
+import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
+
+/**
+ * Login modules that uses properties files similar to the {@link PropertiesLoginModule}. It can
+ * either store the username-password in plain text or in an encrypted/hashed form. the
+ * {@link #main(String[])} method provides a way to prepare unencrypted data to be encrypted/hashed.
+ */
+public class SCRAMPropertiesLoginModule extends PropertiesLoader implements AuditLoginModule {
+
+ /**
+ *
+ */
+ private static final String SEPARATOR_MECHANISM = "|";
+ private static final String SEPARATOR_PARAMETER = ":";
+ private static final int MIN_ITERATIONS = 4096;
+ private static final SecureRandom RANDOM_GENERATOR = new SecureRandom();
+ private Subject subject;
+ private CallbackHandler callbackHandler;
+ private Properties users;
+ private Map<String, Set<String>> roles;
+ private UserData userData;
+ private String user;
+ private final Set<Principal> principals = new HashSet<>();
+
+ @Override
+ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+ Map<String, ?> options) {
+ this.subject = subject;
+ this.callbackHandler = callbackHandler;
+
+ init(options);
+ users = load(PropertiesLoginModule.USER_FILE_PROP_NAME, "user", options).getProps();
+ roles = load(PropertiesLoginModule.ROLE_FILE_PROP_NAME, "role", options).invertedPropertiesValuesMap();
+
+ }
+
+ @Override
+ public boolean login() throws LoginException {
+ NameCallback nameCallback = new NameCallback("Username: ");
+ executeCallbacks(nameCallback);
+ user = nameCallback.getName();
+ SCRAMMechanismCallback mechanismCallback = new SCRAMMechanismCallback();
+ executeCallbacks(mechanismCallback);
+ SCRAM scram = getTypeByString(mechanismCallback.getMechanism());
+ if (user == null) {
+ userData = generateUserData(null); // generate random user data
+ } else {
+ String password = users.getProperty(user + SEPARATOR_MECHANISM + scram.name());
+ if (password == null) {
+ // fallback for probably unencoded user/password or a single encoded entry
+ password = users.getProperty(user);
+ }
+ if (PasswordMaskingUtil.isEncMasked(password)) {
+ String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR_PARAMETER);
+ userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]);
+ } else {
+ userData = generateUserData(password);
+ }
+ }
+ return true;
+ }
+
+ private UserData generateUserData(String plainTextPassword) throws LoginException {
+ if (plainTextPassword == null) {
+ // if the user is not available (or the password) generate a random password here so an
+ // attacker can't
+ // distinguish between a missing username and a wrong password
+ byte[] randomPassword = new byte[256];
+ RANDOM_GENERATOR.nextBytes(randomPassword);
+ plainTextPassword = new String(randomPassword);
+ }
+ DigestCallback digestCallback = new DigestCallback();
+ HmacCallback hmacCallback = new HmacCallback();
+ executeCallbacks(digestCallback, hmacCallback);
+ byte[] salt = generateSalt();
+ try {
+ ScramUtils.NewPasswordStringData data =
+ ScramUtils.byteArrayToStringData(ScramUtils.newPassword(plainTextPassword, salt, 4096,
+ digestCallback.getDigest(),
+ hmacCallback.getHmac()));
+ return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
+ } catch (ScramException e) {
+ throw new LoginException();
+ }
+ }
+
+ private static byte[] generateSalt() {
+ byte[] salt = new byte[32];
+ RANDOM_GENERATOR.nextBytes(salt);
+ return salt;
+ }
+
+ private void executeCallbacks(Callback... callbacks) throws LoginException {
+ try {
+ callbackHandler.handle(callbacks);
+ } catch (UnsupportedCallbackException | IOException e) {
+ throw new LoginException();
+ }
+ }
+
+ @Override
+ public boolean commit() throws LoginException {
+ if (userData == null) {
+ throw new LoginException();
+ }
+ subject.getPublicCredentials().add(userData);
+ Set<UserPrincipal> authenticatedUsers = subject.getPrincipals(UserPrincipal.class);
+ UserPrincipal principal = new UserPrincipal(user);
+ principals.add(principal);
+ authenticatedUsers.add(principal);
+ for (UserPrincipal userPrincipal : authenticatedUsers) {
+ Set<String> matchedRoles = roles.get(userPrincipal.getName());
+ if (matchedRoles != null) {
+ for (String entry : matchedRoles) {
+ principals.add(new RolePrincipal(entry));
+ }
+ }
+ }
+ subject.getPrincipals().addAll(principals);
+ return true;
+ }
+
+ @Override
+ public boolean abort() throws LoginException {
+ return true;
+ }
+
+ @Override
+ public boolean logout() throws LoginException {
+ subject.getPrincipals().removeAll(principals);
+ principals.clear();
+ subject.getPublicCredentials().remove(userData);
+ userData = null;
+ return true;
+ }
+
+ /**
+ * Main method that could be used to encrypt given credentials for use in properties files
+ * @param args username password type [iterations]
+ * @throws GeneralSecurityException if any security mechanism is not available on this JVM
+ * @throws ScramException if invalid data is supplied
+ * @throws StringPrepError if username can't be encoded according to SASL StringPrep
+ * @throws IOException if writing as properties failed
+ */
+ public static void main(String[] args) throws GeneralSecurityException, ScramException, StringPrepError,
+ IOException {
+ if (args.length < 2) {
+ System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() +
+ " <username> <password> [<iterations>]");
+ System.out.println("\ttype: " + getSupportedTypes());
+ System.out.println("\titerations desired number of iteration (min value: " + MIN_ITERATIONS + ")");
+ return;
+ }
+ String username = args[0];
+ String password = args[1];
+ Properties properties = new Properties();
+ String encodedUser = StringPrep.prepAsQueryString(username);
+ for (SCRAM scram : SCRAM.values()) {
+ MessageDigest digest = MessageDigest.getInstance(scram.getDigest());
+ Mac hmac = Mac.getInstance(scram.getHmac());
+ byte[] salt = generateSalt();
+ int iterations;
+ if (args.length > 2) {
+ iterations = Integer.parseInt(args[2]);
+ if (iterations < MIN_ITERATIONS) {
+ throw new IllegalArgumentException("minimum of " + MIN_ITERATIONS + " required!");
+ }
+ } else {
+ iterations = MIN_ITERATIONS;
+ }
+ ScramUtils.NewPasswordStringData data =
+ ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, iterations, digest, hmac));
+ String encodedPassword = PasswordMaskingUtil.wrap(data.salt + SEPARATOR_PARAMETER + data.iterations +
+ SEPARATOR_PARAMETER + data.serverKey + SEPARATOR_PARAMETER + data.storedKey);
+ properties.setProperty(encodedUser + SEPARATOR_MECHANISM + scram.name(), encodedPassword);
+ }
+ properties.store(System.out,
+ "Insert the lines stating with '" + encodedUser + "' into the desired user properties file");
+ }
+
+ private static SCRAM getTypeByString(String type) {
+ SCRAM scram = Arrays.stream(SCRAM.values())
+ .filter(v -> v.getName().equals(type))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("unkown type " + type +
+ ", supported ones are " + getSupportedTypes()));
+ return scram;
+ }
+
+ private static String getSupportedTypes() {
+ return String.join(", ", Arrays.stream(SCRAM.values()).map(SCRAM::getName).toArray(String[]::new));
+ }
+
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/SCRAM.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/SCRAM.java
new file mode 100644
index 0000000..1da4c42
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/SCRAM.java
@@ -0,0 +1,56 @@
+/*
+ * 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.spi.core.security.scram;
+
+/**
+ * Defines sets of known SCRAM types with methods to fetch matching digest and hmac names
+ */
+public enum SCRAM {
+ // ordered by precedence
+ SHA512,
+ SHA256;
+
+ public String getName() {
+ switch (this) {
+ case SHA256:
+ return "SCRAM-SHA-256";
+ case SHA512:
+ return "SCRAM-SHA-512";
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDigest() {
+ switch (this) {
+ case SHA256:
+ return "SHA-256";
+ case SHA512:
+ return "SHA-512";
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ public String getHmac() {
+ switch (this) {
+ case SHA256:
+ return "HmacSHA256";
+ case SHA512:
+ return "HmacSHA512";
+ }
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramException.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramException.java
new file mode 100644
index 0000000..e6ea177
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.spi.core.security.scram;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Indicates error while processing SCRAM sequence
+ */
+public class ScramException extends Exception {
+ /**
+ * Creates new ScramException
+ * @param message Exception message
+ */
+ public ScramException(String message) {
+ super(message);
+ }
+
+ public ScramException(String message, GeneralSecurityException e) {
+ super(message, e);
+ }
+
+ /**
+ * Creates new ScramException
+ * @param cause Throwable
+ */
+ public ScramException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramUtils.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramUtils.java
new file mode 100644
index 0000000..8a98a05
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramUtils.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.spi.core.security.scram;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Provides static methods for working with SCRAM/SASL
+ */
+public class ScramUtils {
+ private static final byte[] INT_1 = new byte[] {0, 0, 0, 1};
+
+ private ScramUtils() {
+ throw new AssertionError("non-instantiable utility class");
+ }
+
+ /**
+ * Generates salted password.
+ * @param password Clear form password, i.e. what user typed
+ * @param salt Salt to be used
+ * @param iterationsCount Iterations for 'salting'
+ * @param mac HMAC to be used
+ * @return salted password
+ * @throws ScramException
+ */
+ public static byte[] generateSaltedPassword(final String password, byte[] salt, int iterationsCount,
+ Mac mac) throws ScramException {
+ SecretKeySpec key = new SecretKeySpec(password.getBytes(StandardCharsets.US_ASCII), mac.getAlgorithm());
+ try {
+ mac.init(key);
+ } catch (InvalidKeyException e) {
+ throw new ScramException("Incompatible key", e);
+ }
+ mac.update(salt);
+ mac.update(INT_1);
+ byte[] result = mac.doFinal();
+
+ byte[] previous = null;
+ for (int i = 1; i < iterationsCount; i++) {
+ mac.update(previous != null ? previous : result);
+ previous = mac.doFinal();
+ for (int x = 0; x < result.length; x++) {
+ result[x] ^= previous[x];
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates HMAC
+ * @param keyBytes key
+ * @param hmacName HMAC name
+ * @return Mac
+ * @throws InvalidKeyException if internal error occur while working with SecretKeySpec
+ * @throws NoSuchAlgorithmException if hmacName is not supported by the java
+ */
+ public static Mac createHmac(final byte[] keyBytes, String hmacName) throws NoSuchAlgorithmException,
+ InvalidKeyException {
+
+ Mac mac = Mac.getInstance(hmacName);
+ SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
+ mac.init(key);
+ return mac;
+ }
+
+ /**
+ * Computes HMAC byte array for given string
+ * @param key key
+ * @param hmacName HMAC name
+ * @param string string for which HMAC will be computed
+ * @return computed HMAC
+ * @throws InvalidKeyException if internal error occur while working with SecretKeySpec
+ * @throws NoSuchAlgorithmException if hmacName is not supported by the java
+ */
+ public static byte[] computeHmac(final byte[] key, String hmacName, final String string) throws InvalidKeyException,
+ NoSuchAlgorithmException {
+
+ Mac mac = createHmac(key, hmacName);
+ mac.update(string.getBytes(StandardCharsets.US_ASCII));
+ return mac.doFinal();
+ }
+
+ public static byte[] computeHmac(final byte[] key, Mac hmac, final String string) throws ScramException {
+
+ try {
+ hmac.init(new SecretKeySpec(key, hmac.getAlgorithm()));
+ } catch (InvalidKeyException e) {
+ throw new ScramException("invalid key", e);
+ }
+ hmac.update(string.getBytes(StandardCharsets.US_ASCII));
+ return hmac.doFinal();
+ }
+
+ /**
+ * Checks if string is null or empty
+ * @param string String to be tested
+ * @return true if the string is null or empty, false otherwise
+ */
+ public static boolean isNullOrEmpty(String string) {
+ return string == null || string.length() == 0; // string.isEmpty() in Java 6
+ }
+
+ /**
+ * Computes the data associated with new password like salted password, keys, etc
+ * <p>
+ * This method is supposed to be used by a server when user provides new clear form password. We
+ * don't want to save it that way so we generate salted password and store it along with other
+ * data required by the SCRAM mechanism
+ * @param passwordClearText Clear form password, i.e. as provided by the user
+ * @param salt Salt to be used
+ * @param iterations Iterations for 'salting'
+ * @param mac HMAC name to be used
+ * @param messageDigest Digest name to be used
+ * @return new password data while working with SecretKeySpec
+ * @throws ScramException
+ */
+ public static NewPasswordByteArrayData newPassword(String passwordClearText, byte[] salt, int iterations,
+ MessageDigest messageDigest, Mac mac) throws ScramException {
+ byte[] saltedPassword = ScramUtils.generateSaltedPassword(passwordClearText, salt, iterations, mac);
+
+ byte[] clientKey = ScramUtils.computeHmac(saltedPassword, mac, "Client Key");
+ byte[] storedKey = messageDigest.digest(clientKey);
+ byte[] serverKey = ScramUtils.computeHmac(saltedPassword, mac, "Server Key");
+
+ return new NewPasswordByteArrayData(saltedPassword, salt, clientKey, storedKey, serverKey, iterations);
+ }
+
+ /**
+ * Transforms NewPasswordByteArrayData into NewPasswordStringData into database friendly (string)
+ * representation Uses Base64 to encode the byte arrays into strings
+ * @param ba Byte array data
+ * @return String data
+ */
+ public static NewPasswordStringData byteArrayToStringData(NewPasswordByteArrayData ba) {
+ return new NewPasswordStringData(Base64.getEncoder().encodeToString(ba.saltedPassword),
+ Base64.getEncoder().encodeToString(ba.salt),
+ Base64.getEncoder().encodeToString(ba.clientKey),
+ Base64.getEncoder().encodeToString(ba.storedKey),
+ Base64.getEncoder().encodeToString(ba.serverKey), ba.iterations);
+ }
+
+ /**
+ * New password data in database friendly format, i.e. Base64 encoded strings
+ */
+ @SuppressWarnings("unused")
+ public static class NewPasswordStringData {
+ /**
+ * Salted password
+ */
+ public final String saltedPassword;
+ /**
+ * Used salt
+ */
+ public final String salt;
+ /**
+ * Client key
+ */
+ public final String clientKey;
+ /**
+ * Stored key
+ */
+ public final String storedKey;
+ /**
+ * Server key
+ */
+ public final String serverKey;
+ /**
+ * Iterations for slating
+ */
+ public final int iterations;
+
+ /**
+ * Creates new NewPasswordStringData
+ * @param saltedPassword Salted password
+ * @param salt Used salt
+ * @param clientKey Client key
+ * @param storedKey Stored key
+ * @param serverKey Server key
+ * @param iterations Iterations for slating
+ */
+ public NewPasswordStringData(String saltedPassword, String salt, String clientKey, String storedKey,
+ String serverKey, int iterations) {
+ this.saltedPassword = saltedPassword;
+ this.salt = salt;
+ this.clientKey = clientKey;
+ this.storedKey = storedKey;
+ this.serverKey = serverKey;
+ this.iterations = iterations;
+ }
+ }
+
+ /**
+ * New password data in byte array format
+ */
+ @SuppressWarnings("unused")
+ public static class NewPasswordByteArrayData {
+ /**
+ * Salted password
+ */
+ public final byte[] saltedPassword;
+ /**
+ * Used salt
+ */
+ public final byte[] salt;
+ /**
+ * Client key
+ */
+ public final byte[] clientKey;
+ /**
+ * Stored key
+ */
+ public final byte[] storedKey;
+ /**
+ * Server key
+ */
+ public final byte[] serverKey;
+ /**
+ * Iterations for slating
+ */
+ public final int iterations;
+
+ /**
+ * Creates new NewPasswordByteArrayData
+ * @param saltedPassword Salted password
+ * @param salt Used salt
+ * @param clientKey Client key
+ * @param storedKey Stored key
+ * @param serverKey Server key
+ * @param iterations Iterations for slating
+ */
+ public NewPasswordByteArrayData(byte[] saltedPassword, byte[] salt, byte[] clientKey, byte[] storedKey,
+ byte[] serverKey, int iterations) {
+
+ this.saltedPassword = saltedPassword;
+ this.salt = salt;
+ this.clientKey = clientKey;
+ this.storedKey = storedKey;
+ this.serverKey = serverKey;
+ this.iterations = iterations;
+ }
+ }
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java
new file mode 100644
index 0000000..8c73752
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java
@@ -0,0 +1,2139 @@
+/**
+ * Copyright 2011 Glenn Maynard
+ * <p>
+ * All rights reserved. Licensed 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.spi.core.security.scram;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * rfc3454 StringPrep, with an implementation of rfc4013 SASLPrep.
+ * <p>
+ * StringPrep case folding is unimplemented, as it's not required by SASLPrep.
+ */
+public class StringPrep {
+ /**
+ * A representation of sets of character classes.
+ */
+ protected static class CharClass {
+ // Each character class is a set of [start,end] tuples; each tuple is represented
+ // in the mapping as mapping[start] = (end-start+1).
+ // Invariants:
+ // - tupleStart is in ascending order.
+ // - All values in tupleCount are >= 1 (no empty ranges).
+ // - tupleStart.size() == tupleCount.size().
+ // - There will be no overlapping ranges.
+ //
+ // TreeMap would work well for this, but it was missing basic operations like lowerEntry
+ // until JDK1.6, which we don't want to depend on.
+ private final ArrayList<Integer> tupleStart = new ArrayList<>();
+ private final ArrayList<Integer> tupleCount = new ArrayList<>();
+
+ static CharClass fromList(int[] charMap) {
+ SortedMap<Integer, Integer> mapping = new TreeMap<>();
+ for (int element : charMap)
+ mapping.put(element, 1);
+
+ return new CharClass(mapping);
+ }
+
+ static CharClass fromRanges(int[] charMap) {
+ // There must be an even number of tuples in RANGES tables.
+ if ((charMap.length % 2) != 0)
+ throw new IllegalArgumentException("Invalid character list size");
+
+ SortedMap<Integer, Integer> mapping = new TreeMap<>();
+ for (int i = 0; i < charMap.length; i += 2) {
+ int start = charMap[i];
+ int end = charMap[i + 1];
+ int count = end - start + 1;
+ mapping.put(start, count);
+ }
+
+ return new CharClass(mapping);
+ }
+
+ static CharClass fromClasses(CharClass... classes) {
+ SortedMap<Integer, Integer> mapping = new TreeMap<>();
+ for (CharClass charClass : classes) {
+ for (int i = 0; i < charClass.tupleStart.size(); ++i) {
+ int start = charClass.tupleStart.get(i);
+ int count = charClass.tupleCount.get(i);
+ mapping.put(start, count);
+ }
+ }
+
+ return new CharClass(mapping);
+ }
+
+ private CharClass(SortedMap<Integer, Integer> mappings) {
+ for (Map.Entry<Integer, Integer> pair : mappings.entrySet()) {
+ int start = pair.getKey();
+ int count = pair.getValue();
+
+ // Coalesce overlapping ranges.
+ if (tupleStart.size() > 0) {
+ int prevIndex = tupleStart.size() - 1;
+ int prevStart = tupleStart.get(prevIndex);
+ int prevCount = tupleCount.get(prevIndex);
+ // If the previous tuple is (0,1), and this tuple is (1,5), then
+ // coalesce into (0,6). If ths previous tuple is (0,3) and this
+ // tuple is (1,3), coalesce into (0,4).
+ if (prevStart + prevCount >= start) {
+ int endPos = start + count;
+ int newCount = endPos - prevStart;
+ tupleCount.set(prevIndex, newCount);
+ continue;
+ }
+ }
+
+ tupleStart.add(start);
+ tupleCount.add(count);
+ }
+ }
+
+ public boolean isCharInClass(int c) {
+ // Find the first entry in tupleStart which is <= c. Java's binarySearch
+ // API is a bit braindamaged (it was written only considering search-and-insert),
+ // so we have to jump some hoops to get this.
+ int pos = Collections.binarySearch(tupleStart, c);
+ if (pos >= 0) {
+ // If pos >= 0, tupleStart[pos] == c. The value is the start of a range,
+ // so it's included in the class.
+ return true;
+ // while(pos > 0 && tupleStart.get(pos-1) == c)
+ // --pos;
+ }
+ // -pos - 1 is the lowest index where tupleStart[pos] > c. If this is the
+ // first entry, then c is below all entries in the class.
+ pos = -pos - 1;
+ if (pos == 0)
+ return false;
+ --pos;
+
+ // tupleStart[pos] is <= c.
+ int start = tupleStart.get(pos);
+ int count = tupleCount.get(pos);
+ return start <= c && c < start + count;
+
+ }
+ }
+
+ /** A.1 Unassigned code points in Unicode 3.2 */
+ static final CharClass A1 = CharClass.fromRanges(new int[] {0x0221,
+ 0x0221,
+ 0x0234,
+ 0x024F,
+ 0x02AE,
+ 0x02AF,
+ 0x02EF,
+ 0x02FF,
+ 0x0350,
+ 0x035F,
+ 0x0370,
+ 0x0373,
+ 0x0376,
+ 0x0379,
+ 0x037B,
+ 0x037D,
+ 0x037F,
+ 0x0383,
+ 0x038B,
+ 0x038B,
+ 0x038D,
+ 0x038D,
+ 0x03A2,
+ 0x03A2,
+ 0x03CF,
+ 0x03CF,
+ 0x03F7,
+ 0x03FF,
+ 0x0487,
+ 0x0487,
+ 0x04CF,
+ 0x04CF,
+ 0x04F6,
+ 0x04F7,
+ 0x04FA,
+ 0x04FF,
+ 0x0510,
+ 0x0530,
+ 0x0557,
+ 0x0558,
+ 0x0560,
+ 0x0560,
+ 0x0588,
+ 0x0588,
+ 0x058B,
+ 0x0590,
+ 0x05A2,
+ 0x05A2,
+ 0x05BA,
+ 0x05BA,
+ 0x05C5,
+ 0x05CF,
+ 0x05EB,
+ 0x05EF,
+ 0x05F5,
+ 0x060B,
+ 0x060D,
+ 0x061A,
+ 0x061C,
+ 0x061E,
+ 0x0620,
+ 0x0620,
+ 0x063B,
+ 0x063F,
+ 0x0656,
+ 0x065F,
+ 0x06EE,
+ 0x06EF,
+ 0x06FF,
+ 0x06FF,
+ 0x070E,
+ 0x070E,
+ 0x072D,
+ 0x072F,
+ 0x074B,
+ 0x077F,
+ 0x07B2,
+ 0x0900,
+ 0x0904,
+ 0x0904,
+ 0x093A,
+ 0x093B,
+ 0x094E,
+ 0x094F,
+ 0x0955,
+ 0x0957,
+ 0x0971,
+ 0x0980,
+ 0x0984,
+ 0x0984,
+ 0x098D,
+ 0x098E,
+ 0x0991,
+ 0x0992,
+ 0x09A9,
+ 0x09A9,
+ 0x09B1,
+ 0x09B1,
+ 0x09B3,
+ 0x09B5,
+ 0x09BA,
+ 0x09BB,
+ 0x09BD,
+ 0x09BD,
+ 0x09C5,
+ 0x09C6,
+ 0x09C9,
+ 0x09CA,
+ 0x09CE,
+ 0x09D6,
+ 0x09D8,
+ 0x09DB,
+ 0x09DE,
+ 0x09DE,
+ 0x09E4,
+ 0x09E5,
+ 0x09FB,
+ 0x0A01,
+ 0x0A03,
+ 0x0A04,
+ 0x0A0B,
+ 0x0A0E,
+ 0x0A11,
+ 0x0A12,
+ 0x0A29,
+ 0x0A29,
+ 0x0A31,
+ 0x0A31,
+ 0x0A34,
+ 0x0A34,
+ 0x0A37,
+ 0x0A37,
+ 0x0A3A,
+ 0x0A3B,
+ 0x0A3D,
+ 0x0A3D,
+ 0x0A43,
+ 0x0A46,
+ 0x0A49,
+ 0x0A4A,
+ 0x0A4E,
+ 0x0A58,
+ 0x0A5D,
+ 0x0A5D,
+ 0x0A5F,
+ 0x0A65,
+ 0x0A75,
+ 0x0A80,
+ 0x0A84,
+ 0x0A84,
+ 0x0A8C,
+ 0x0A8C,
+ 0x0A8E,
+ 0x0A8E,
+ 0x0A92,
+ 0x0A92,
+ 0x0AA9,
+ 0x0AA9,
+ 0x0AB1,
+ 0x0AB1,
+ 0x0AB4,
+ 0x0AB4,
+ 0x0ABA,
+ 0x0ABB,
+ 0x0AC6,
+ 0x0AC6,
+ 0x0ACA,
+ 0x0ACA,
+ 0x0ACE,
+ 0x0ACF,
+ 0x0AD1,
+ 0x0ADF,
+ 0x0AE1,
+ 0x0AE5,
+ 0x0AF0,
+ 0x0B00,
+ 0x0B04,
+ 0x0B04,
+ 0x0B0D,
+ 0x0B0E,
+ 0x0B11,
+ 0x0B12,
+ 0x0B29,
+ 0x0B29,
+ 0x0B31,
+ 0x0B31,
+ 0x0B34,
+ 0x0B35,
+ 0x0B3A,
+ 0x0B3B,
+ 0x0B44,
+ 0x0B46,
+ 0x0B49,
+ 0x0B4A,
+ 0x0B4E,
+ 0x0B55,
+ 0x0B58,
+ 0x0B5B,
+ 0x0B5E,
+ 0x0B5E,
+ 0x0B62,
+ 0x0B65,
+ 0x0B71,
+ 0x0B81,
+ 0x0B84,
+ 0x0B84,
+ 0x0B8B,
+ 0x0B8D,
+ 0x0B91,
+ 0x0B91,
+ 0x0B96,
+ 0x0B98,
+ 0x0B9B,
+ 0x0B9B,
+ 0x0B9D,
+ 0x0B9D,
+ 0x0BA0,
+ 0x0BA2,
+ 0x0BA5,
+ 0x0BA7,
+ 0x0BAB,
+ 0x0BAD,
+ 0x0BB6,
+ 0x0BB6,
+ 0x0BBA,
+ 0x0BBD,
+ 0x0BC3,
+ 0x0BC5,
+ 0x0BC9,
+ 0x0BC9,
+ 0x0BCE,
+ 0x0BD6,
+ 0x0BD8,
+ 0x0BE6,
+ 0x0BF3,
+ 0x0C00,
+ 0x0C04,
+ 0x0C04,
+ 0x0C0D,
+ 0x0C0D,
+ 0x0C11,
+ 0x0C11,
+ 0x0C29,
+ 0x0C29,
+ 0x0C34,
+ 0x0C34,
+ 0x0C3A,
+ 0x0C3D,
+ 0x0C45,
+ 0x0C45,
+ 0x0C49,
+ 0x0C49,
+ 0x0C4E,
+ 0x0C54,
+ 0x0C57,
+ 0x0C5F,
+ 0x0C62,
+ 0x0C65,
+ 0x0C70,
+ 0x0C81,
+ 0x0C84,
+ 0x0C84,
+ 0x0C8D,
+ 0x0C8D,
+ 0x0C91,
+ 0x0C91,
+ 0x0CA9,
+ 0x0CA9,
+ 0x0CB4,
+ 0x0CB4,
+ 0x0CBA,
+ 0x0CBD,
+ 0x0CC5,
+ 0x0CC5,
+ 0x0CC9,
+ 0x0CC9,
+ 0x0CCE,
+ 0x0CD4,
+ 0x0CD7,
+ 0x0CDD,
+ 0x0CDF,
+ 0x0CDF,
+ 0x0CE2,
+ 0x0CE5,
+ 0x0CF0,
+ 0x0D01,
+ 0x0D04,
+ 0x0D04,
+ 0x0D0D,
+ 0x0D0D,
+ 0x0D11,
+ 0x0D11,
+ 0x0D29,
+ 0x0D29,
+ 0x0D3A,
+ 0x0D3D,
+ 0x0D44,
+ 0x0D45,
+ 0x0D49,
+ 0x0D49,
+ 0x0D4E,
+ 0x0D56,
+ 0x0D58,
+ 0x0D5F,
+ 0x0D62,
+ 0x0D65,
+ 0x0D70,
+ 0x0D81,
+ 0x0D84,
+ 0x0D84,
+ 0x0D97,
+ 0x0D99,
+ 0x0DB2,
+ 0x0DB2,
+ 0x0DBC,
+ 0x0DBC,
+ 0x0DBE,
+ 0x0DBF,
+ 0x0DC7,
+ 0x0DC9,
+ 0x0DCB,
+ 0x0DCE,
+ 0x0DD5,
+ 0x0DD5,
+ 0x0DD7,
+ 0x0DD7,
+ 0x0DE0,
+ 0x0DF1,
+ 0x0DF5,
+ 0x0E00,
+ 0x0E3B,
+ 0x0E3E,
+ 0x0E5C,
+ 0x0E80,
+ 0x0E83,
+ 0x0E83,
+ 0x0E85,
+ 0x0E86,
+ 0x0E89,
+ 0x0E89,
+ 0x0E8B,
+ 0x0E8C,
+ 0x0E8E,
+ 0x0E93,
+ 0x0E98,
+ 0x0E98,
+ 0x0EA0,
+ 0x0EA0,
+ 0x0EA4,
+ 0x0EA4,
+ 0x0EA6,
+ 0x0EA6,
+ 0x0EA8,
+ 0x0EA9,
+ 0x0EAC,
+ 0x0EAC,
+ 0x0EBA,
+ 0x0EBA,
+ 0x0EBE,
+ 0x0EBF,
+ 0x0EC5,
+ 0x0EC5,
+ 0x0EC7,
+ 0x0EC7,
+ 0x0ECE,
+ 0x0ECF,
+ 0x0EDA,
+ 0x0EDB,
+ 0x0EDE,
+ 0x0EFF,
+ 0x0F48,
+ 0x0F48,
+ 0x0F6B,
+ 0x0F70,
+ 0x0F8C,
+ 0x0F8F,
+ 0x0F98,
+ 0x0F98,
+ 0x0FBD,
+ 0x0FBD,
+ 0x0FCD,
+ 0x0FCE,
+ 0x0FD0,
+ 0x0FFF,
+ 0x1022,
+ 0x1022,
+ 0x1028,
+ 0x1028,
+ 0x102B,
+ 0x102B,
+ 0x1033,
+ 0x1035,
+ 0x103A,
+ 0x103F,
+ 0x105A,
+ 0x109F,
+ 0x10C6,
+ 0x10CF,
+ 0x10F9,
+ 0x10FA,
+ 0x10FC,
+ 0x10FF,
+ 0x115A,
+ 0x115E,
+ 0x11A3,
+ 0x11A7,
+ 0x11FA,
+ 0x11FF,
+ 0x1207,
+ 0x1207,
+ 0x1247,
+ 0x1247,
+ 0x1249,
+ 0x1249,
+ 0x124E,
+ 0x124F,
+ 0x1257,
+ 0x1257,
+ 0x1259,
+ 0x1259,
+ 0x125E,
+ 0x125F,
+ 0x1287,
+ 0x1287,
+ 0x1289,
+ 0x1289,
+ 0x128E,
+ 0x128F,
+ 0x12AF,
+ 0x12AF,
+ 0x12B1,
+ 0x12B1,
+ 0x12B6,
+ 0x12B7,
+ 0x12BF,
+ 0x12BF,
+ 0x12C1,
+ 0x12C1,
+ 0x12C6,
+ 0x12C7,
+ 0x12CF,
+ 0x12CF,
+ 0x12D7,
+ 0x12D7,
+ 0x12EF,
+ 0x12EF,
+ 0x130F,
+ 0x130F,
+ 0x1311,
+ 0x1311,
+ 0x1316,
+ 0x1317,
+ 0x131F,
+ 0x131F,
+ 0x1347,
+ 0x1347,
+ 0x135B,
+ 0x1360,
+ 0x137D,
+ 0x139F,
+ 0x13F5,
+ 0x1400,
+ 0x1677,
+ 0x167F,
+ 0x169D,
+ 0x169F,
+ 0x16F1,
+ 0x16FF,
+ 0x170D,
+ 0x170D,
+ 0x1715,
+ 0x171F,
+ 0x1737,
+ 0x173F,
+ 0x1754,
+ 0x175F,
+ 0x176D,
+ 0x176D,
+ 0x1771,
+ 0x1771,
+ 0x1774,
+ 0x177F,
+ 0x17DD,
+ 0x17DF,
+ 0x17EA,
+ 0x17FF,
+ 0x180F,
+ 0x180F,
+ 0x181A,
+ 0x181F,
+ 0x1878,
+ 0x187F,
+ 0x18AA,
+ 0x1DFF,
+ 0x1E9C,
+ 0x1E9F,
+ 0x1EFA,
+ 0x1EFF,
+ 0x1F16,
+ 0x1F17,
+ 0x1F1E,
+ 0x1F1F,
+ 0x1F46,
+ 0x1F47,
+ 0x1F4E,
+ 0x1F4F,
+ 0x1F58,
+ 0x1F58,
+ 0x1F5A,
+ 0x1F5A,
+ 0x1F5C,
+ 0x1F5C,
+ 0x1F5E,
+ 0x1F5E,
+ 0x1F7E,
+ 0x1F7F,
+ 0x1FB5,
+ 0x1FB5,
+ 0x1FC5,
+ 0x1FC5,
+ 0x1FD4,
+ 0x1FD5,
+ 0x1FDC,
+ 0x1FDC,
+ 0x1FF0,
+ 0x1FF1,
+ 0x1FF5,
+ 0x1FF5,
+ 0x1FFF,
+ 0x1FFF,
+ 0x2053,
+ 0x2056,
+ 0x2058,
+ 0x205E,
+ 0x2064,
+ 0x2069,
+ 0x2072,
+ 0x2073,
+ 0x208F,
+ 0x209F,
+ 0x20B2,
+ 0x20CF,
+ 0x20EB,
+ 0x20FF,
+ 0x213B,
+ 0x213C,
+ 0x214C,
+ 0x2152,
+ 0x2184,
+ 0x218F,
+ 0x23CF,
+ 0x23FF,
+ 0x2427,
+ 0x243F,
+ 0x244B,
+ 0x245F,
+ 0x24FF,
+ 0x24FF,
+ 0x2614,
+ 0x2615,
+ 0x2618,
+ 0x2618,
+ 0x267E,
+ 0x267F,
+ 0x268A,
+ 0x2700,
+ 0x2705,
+ 0x2705,
+ 0x270A,
+ 0x270B,
+ 0x2728,
+ 0x2728,
+ 0x274C,
+ 0x274C,
+ 0x274E,
+ 0x274E,
+ 0x2753,
+ 0x2755,
+ 0x2757,
+ 0x2757,
+ 0x275F,
+ 0x2760,
+ 0x2795,
+ 0x2797,
+ 0x27B0,
+ 0x27B0,
+ 0x27BF,
+ 0x27CF,
+ 0x27EC,
+ 0x27EF,
+ 0x2B00,
+ 0x2E7F,
+ 0x2E9A,
+ 0x2E9A,
+ 0x2EF4,
+ 0x2EFF,
+ 0x2FD6,
+ 0x2FEF,
+ 0x2FFC,
+ 0x2FFF,
+ 0x3040,
+ 0x3040,
+ 0x3097,
+ 0x3098,
+ 0x3100,
+ 0x3104,
+ 0x312D,
+ 0x3130,
+ 0x318F,
+ 0x318F,
+ 0x31B8,
+ 0x31EF,
+ 0x321D,
+ 0x321F,
+ 0x3244,
+ 0x3250,
+ 0x327C,
+ 0x327E,
+ 0x32CC,
+ 0x32CF,
+ 0x32FF,
+ 0x32FF,
+ 0x3377,
+ 0x337A,
+ 0x33DE,
+ 0x33DF,
+ 0x33FF,
+ 0x33FF,
+ 0x4DB6,
+ 0x4DFF,
+ 0x9FA6,
+ 0x9FFF,
+ 0xA48D,
+ 0xA48F,
+ 0xA4C7,
+ 0xABFF,
+ 0xD7A4,
+ 0xD7FF,
+ 0xFA2E,
+ 0xFA2F,
+ 0xFA6B,
+ 0xFAFF,
+ 0xFB07,
+ 0xFB12,
+ 0xFB18,
+ 0xFB1C,
+ 0xFB37,
+ 0xFB37,
+ 0xFB3D,
+ 0xFB3D,
+ 0xFB3F,
+ 0xFB3F,
+ 0xFB42,
+ 0xFB42,
+ 0xFB45,
+ 0xFB45,
+ 0xFBB2,
+ 0xFBD2,
+ 0xFD40,
+ 0xFD4F,
+ 0xFD90,
+ 0xFD91,
+ 0xFDC8,
+ 0xFDCF,
+ 0xFDFD,
+ 0xFDFF,
+ 0xFE10,
+ 0xFE1F,
+ 0xFE24,
+ 0xFE2F,
+ 0xFE47,
+ 0xFE48,
+ 0xFE53,
+ 0xFE53,
+ 0xFE67,
+ 0xFE67,
+ 0xFE6C,
+ 0xFE6F,
+ 0xFE75,
+ 0xFE75,
+ 0xFEFD,
+ 0xFEFE,
+ 0xFF00,
+ 0xFF00,
+ 0xFFBF,
+ 0xFFC1,
+ 0xFFC8,
+ 0xFFC9,
+ 0xFFD0,
+ 0xFFD1,
+ 0xFFD8,
+ 0xFFD9,
+ 0xFFDD,
+ 0xFFDF,
+ 0xFFE7,
+ 0xFFE7,
+ 0xFFEF,
+ 0xFFF8,
+ 0x10000,
+ 0x102FF,
+ 0x1031F,
+ 0x1031F,
+ 0x10324,
+ 0x1032F,
+ 0x1034B,
+ 0x103FF,
+ 0x10426,
+ 0x10427,
+ 0x1044E,
+ 0x1CFFF,
+ 0x1D0F6,
+ 0x1D0FF,
+ 0x1D127,
+ 0x1D129,
+ 0x1D1DE,
+ 0x1D3FF,
+ 0x1D455,
+ 0x1D455,
+ 0x1D49D,
+ 0x1D49D,
+ 0x1D4A0,
+ 0x1D4A1,
+ 0x1D4A3,
+ 0x1D4A4,
+ 0x1D4A7,
+ 0x1D4A8,
+ 0x1D4AD,
+ 0x1D4AD,
+ 0x1D4BA,
+ 0x1D4BA,
+ 0x1D4BC,
+ 0x1D4BC,
+ 0x1D4C1,
+ 0x1D4C1,
+ 0x1D4C4,
+ 0x1D4C4,
+ 0x1D506,
+ 0x1D506,
+ 0x1D50B,
+ 0x1D50C,
+ 0x1D515,
+ 0x1D515,
+ 0x1D51D,
+ 0x1D51D,
+ 0x1D53A,
+ 0x1D53A,
+ 0x1D53F,
+ 0x1D53F,
+ 0x1D545,
+ 0x1D545,
+ 0x1D547,
+ 0x1D549,
+ 0x1D551,
+ 0x1D551,
+ 0x1D6A4,
+ 0x1D6A7,
+ 0x1D7CA,
+ 0x1D7CD,
+ 0x1D800,
+ 0x1FFFD,
+ 0x2A6D7,
+ 0x2F7FF,
+ 0x2FA1E,
+ 0x2FFFD,
+ 0x30000,
+ 0x3FFFD,
+ 0x40000,
+ 0x4FFFD,
+ 0x50000,
+ 0x5FFFD,
+ 0x60000,
+ 0x6FFFD,
+ 0x70000,
+ 0x7FFFD,
+ 0x80000,
+ 0x8FFFD,
+ 0x90000,
+ 0x9FFFD,
+ 0xA0000,
+ 0xAFFFD,
+ 0xB0000,
+ 0xBFFFD,
+ 0xC0000,
+ 0xCFFFD,
+ 0xD0000,
+ 0xDFFFD,
+ 0xE0000,
+ 0xE0000,
+ 0xE0002,
+ 0xE001F,
+ 0xE0080,
+ 0xEFFFD,});
+
+ /** B.1 Commonly mapped to nothing */
+ static final CharClass B1 = CharClass.fromList(new int[] {0x00AD,
+ 0x034F,
+ 0x1806,
+ 0x180B,
+ 0x180C,
+ 0x180D,
+ 0x200B,
+ 0x200C,
+ 0x200D,
+ 0x2060,
+ 0xFE00,
+ 0xFE01,
+ 0xFE02,
+ 0xFE03,
+ 0xFE04,
+ 0xFE05,
+ 0xFE06,
+ 0xFE07,
+ 0xFE08,
+ 0xFE09,
+ 0xFE0A,
+ 0xFE0B,
+ 0xFE0C,
+ 0xFE0D,
+ 0xFE0E,
+ 0xFE0F,
+ 0xFEFF,});
+
+ /** C.1.1 ASCII space characters */
+ static final CharClass C11 = CharClass.fromList(new int[] {0x0020});
+
+ /** C.1.2 Non-ASCII space characters */
+ static final CharClass C12 = CharClass.fromList(new int[] {0x00A0,
+ 0x1680,
+ 0x2000,
+ 0x2001,
+ 0x2002,
+ 0x2003,
+ 0x2004,
+ 0x2005,
+ 0x2006,
+ 0x2007,
+ 0x2008,
+ 0x2009,
+ 0x200A,
+ 0x200B,
+ 0x202F,
+ 0x205F,
+ 0x3000,});
+
+ /** C.2.1 ASCII control characters */
+ static final CharClass C21 = CharClass.fromList(new int[] {0x0000,
+ 0x0001,
+ 0x0002,
+ 0x0003,
+ 0x0004,
+ 0x0005,
+ 0x0006,
+ 0x0007,
+ 0x0008,
+ 0x0009,
+ 0x000A,
+ 0x000B,
+ 0x000C,
+ 0x000D,
+ 0x000E,
+ 0x000F,
+ 0x0010,
+ 0x0011,
+ 0x0012,
+ 0x0013,
+ 0x0014,
+ 0x0015,
+ 0x0016,
+ 0x0017,
+ 0x0018,
+ 0x0019,
+ 0x001A,
+ 0x001B,
+ 0x001C,
+ 0x001D,
+ 0x001E,
+ 0x001F,
+ 0x007F});
+
+ /** C.2.2 Non-ASCII control characters */
+ static final CharClass C22 = CharClass.fromList(new int[] {0x0080,
+ 0x0081,
+ 0x0082,
+ 0x0083,
+ 0x0084,
+ 0x0085,
+ 0x0086,
+ 0x0087,
+ 0x0088,
+ 0x0089,
+ 0x008A,
+ 0x008B,
+ 0x008C,
+ 0x008D,
+ 0x008E,
+ 0x008F,
+ 0x0090,
+ 0x0091,
+ 0x0092,
+ 0x0093,
+ 0x0094,
+ 0x0095,
+ 0x0096,
+ 0x0097,
+ 0x0098,
+ 0x0099,
+ 0x009A,
+ 0x009B,
+ 0x009C,
+ 0x009D,
+ 0x009E,
+ 0x009F,
+ 0x06DD,
+ 0x070F,
+ 0x180E,
+ 0x200C,
+ 0x200D,
+ 0x2028,
+ 0x2029,
+ 0x2060,
+ 0x2061,
+ 0x2062,
+ 0x2063,
+ 0x206A,
+ 0x206B,
+ 0x206C,
+ 0x206D,
+ 0x206E,
+ 0x206F,
+ 0xFEFF,
+ 0xFFF9,
+ 0xFFFA,
+ 0xFFFB,
+ 0xFFFC,
+ 0x1D173,
+ 0x1D174,
+ 0x1D175,
+ 0x1D176,
+ 0x1D177,
+ 0x1D178,
+ 0x1D179,
+ 0x1D17A,});
+
+ /** C.3 Private use */
+ static final CharClass C3 = CharClass.fromRanges(new int[] {0xE000, 0xF8FF, 0xF0000, 0xFFFFD, 0x100000, 0x10FFFD,});
+
+ /** C.4 Non-character code points */
+ static final CharClass C4 = CharClass.fromRanges(new int[] {0xFDD0,
+ 0xFDEF,
+ 0xFFFE,
+ 0xFFFF,
+ 0x1FFFE,
+ 0x1FFFF,
+ 0x2FFFE,
+ 0x2FFFF,
+ 0x3FFFE,
+ 0x3FFFF,
+ 0x4FFFE,
+ 0x4FFFF,
+ 0x5FFFE,
+ 0x5FFFF,
+ 0x6FFFE,
+ 0x6FFFF,
+ 0x7FFFE,
+ 0x7FFFF,
+ 0x8FFFE,
+ 0x8FFFF,
+ 0x9FFFE,
+ 0x9FFFF,
+ 0xAFFFE,
+ 0xAFFFF,
+ 0xBFFFE,
+ 0xBFFFF,
+ 0xCFFFE,
+ 0xCFFFF,
+ 0xDFFFE,
+ 0xDFFFF,
+ 0xEFFFE,
+ 0xEFFFF,
+ 0xFFFFE,
+ 0xFFFFF,
+ 0x10FFFE,
+ 0x10FFFF,});
+
+ /** C.5 Surrogate codes */
+ static final CharClass C5 = CharClass.fromRanges(new int[] {0xD800, 0xDFFF,});
+
+ /** C.6 Inappropriate for plain text */
+ static final CharClass C6 = CharClass.fromList(new int[] {0xFFF9, 0xFFFA, 0xFFFB, 0xFFFC, 0xFFFD,});
+
+ /** C.7 Inappropriate for canonical representation */
+ static final CharClass C7 = CharClass.fromList(new int[] {0x2FF0,
+ 0x2FF1,
+ 0x2FF2,
+ 0x2FF3,
+ 0x2FF4,
+ 0x2FF5,
+ 0x2FF6,
+ 0x2FF7,
+ 0x2FF8,
+ 0x2FF9,
+ 0x2FFA,
+ 0x2FFB,});
+
+ /** C.8 Change display properties or are deprecated */
+ static final CharClass C8 = CharClass.fromList(new int[] {0x0340,
+ 0x0341,
+ 0x200E,
+ 0x200F,
+ 0x202A,
+ 0x202B,
+ 0x202C,
+ 0x202D,
+ 0x202E,
+ 0x206A,
+ 0x206B,
+ 0x206C,
+ 0x206D,
+ 0x206E,
+ 0x206F,});
+
+ /** C.9 Tagging characters (tuples) */
+ static final CharClass C9 = CharClass.fromRanges(new int[] {0xE0001, 0xE0001, 0xE0020, 0xE007F,});
+
+ /** D.1 Characters with bidirectional property "R" or "AL" */
+ static final CharClass D1 = CharClass.fromRanges(new int[] {0x05BE,
+ 0x05BE,
+ 0x05C0,
+ 0x05C0,
+ 0x05C3,
+ 0x05C3,
+ 0x05D0,
+ 0x05EA,
+ 0x05F0,
+ 0x05F4,
+ 0x061B,
+ 0x061B,
+ 0x061F,
+ 0x061F,
+ 0x0621,
+ 0x063A,
+ 0x0640,
+ 0x064A,
+ 0x066D,
+ 0x066F,
+ 0x0671,
+ 0x06D5,
+ 0x06DD,
+ 0x06DD,
+ 0x06E5,
+ 0x06E6,
+ 0x06FA,
+ 0x06FE,
+ 0x0700,
+ 0x070D,
+ 0x0710,
+ 0x0710,
+ 0x0712,
+ 0x072C,
+ 0x0780,
+ 0x07A5,
+ 0x07B1,
+ 0x07B1,
+ 0x200F,
+ 0x200F,
+ 0xFB1D,
+ 0xFB1D,
+ 0xFB1F,
+ 0xFB28,
+ 0xFB2A,
+ 0xFB36,
+ 0xFB38,
+ 0xFB3C,
+ 0xFB3E,
+ 0xFB3E,
+ 0xFB40,
+ 0xFB41,
+ 0xFB43,
+ 0xFB44,
+ 0xFB46,
+ 0xFBB1,
+ 0xFBD3,
+ 0xFD3D,
+ 0xFD50,
+ 0xFD8F,
+ 0xFD92,
+ 0xFDC7,
+ 0xFDF0,
+ 0xFDFC,
+ 0xFE70,
+ 0xFE74,
+ 0xFE76,
+ 0xFEFC,});
+
+ /** D.2 Characters with bidirectional property "L" */
+ static final CharClass D2 = CharClass.fromRanges(new int[] {0x0041,
+ 0x005A,
+ 0x0061,
+ 0x007A,
+ 0x00AA,
+ 0x00AA,
+ 0x00B5,
+ 0x00B5,
+ 0x00BA,
+ 0x00BA,
+ 0x00C0,
+ 0x00D6,
+ 0x00D8,
+ 0x00F6,
+ 0x00F8,
+ 0x0220,
+ 0x0222,
+ 0x0233,
+ 0x0250,
+ 0x02AD,
+ 0x02B0,
+ 0x02B8,
+ 0x02BB,
+ 0x02C1,
+ 0x02D0,
+ 0x02D1,
+ 0x02E0,
+ 0x02E4,
+ 0x02EE,
+ 0x02EE,
+ 0x037A,
+ 0x037A,
+ 0x0386,
+ 0x0386,
+ 0x0388,
+ 0x038A,
+ 0x038C,
+ 0x038C,
+ 0x038E,
+ 0x03A1,
+ 0x03A3,
+ 0x03CE,
+ 0x03D0,
+ 0x03F5,
+ 0x0400,
+ 0x0482,
+ 0x048A,
+ 0x04CE,
+ 0x04D0,
+ 0x04F5,
+ 0x04F8,
+ 0x04F9,
+ 0x0500,
+ 0x050F,
+ 0x0531,
+ 0x0556,
+ 0x0559,
+ 0x055F,
+ 0x0561,
+ 0x0587,
+ 0x0589,
+ 0x0589,
+ 0x0903,
+ 0x0903,
+ 0x0905,
+ 0x0939,
+ 0x093D,
+ 0x0940,
+ 0x0949,
+ 0x094C,
+ 0x0950,
+ 0x0950,
+ 0x0958,
+ 0x0961,
+ 0x0964,
+ 0x0970,
+ 0x0982,
+ 0x0983,
+ 0x0985,
+ 0x098C,
+ 0x098F,
+ 0x0990,
+ 0x0993,
+ 0x09A8,
+ 0x09AA,
+ 0x09B0,
+ 0x09B2,
+ 0x09B2,
+ 0x09B6,
+ 0x09B9,
+ 0x09BE,
+ 0x09C0,
+ 0x09C7,
+ 0x09C8,
+ 0x09CB,
+ 0x09CC,
+ 0x09D7,
+ 0x09D7,
+ 0x09DC,
+ 0x09DD,
+ 0x09DF,
+ 0x09E1,
+ 0x09E6,
+ 0x09F1,
+ 0x09F4,
+ 0x09FA,
+ 0x0A05,
+ 0x0A0A,
+ 0x0A0F,
+ 0x0A10,
+ 0x0A13,
+ 0x0A28,
+ 0x0A2A,
+ 0x0A30,
+ 0x0A32,
+ 0x0A33,
+ 0x0A35,
+ 0x0A36,
+ 0x0A38,
+ 0x0A39,
+ 0x0A3E,
+ 0x0A40,
+ 0x0A59,
+ 0x0A5C,
+ 0x0A5E,
+ 0x0A5E,
+ 0x0A66,
+ 0x0A6F,
+ 0x0A72,
+ 0x0A74,
+ 0x0A83,
+ 0x0A83,
+ 0x0A85,
+ 0x0A8B,
+ 0x0A8D,
+ 0x0A8D,
+ 0x0A8F,
+ 0x0A91,
+ 0x0A93,
+ 0x0AA8,
+ 0x0AAA,
+ 0x0AB0,
+ 0x0AB2,
+ 0x0AB3,
+ 0x0AB5,
+ 0x0AB9,
+ 0x0ABD,
+ 0x0AC0,
+ 0x0AC9,
+ 0x0AC9,
+ 0x0ACB,
+ 0x0ACC,
+ 0x0AD0,
+ 0x0AD0,
+ 0x0AE0,
+ 0x0AE0,
+ 0x0AE6,
+ 0x0AEF,
+ 0x0B02,
+ 0x0B03,
+ 0x0B05,
+ 0x0B0C,
+ 0x0B0F,
+ 0x0B10,
+ 0x0B13,
+ 0x0B28,
+ 0x0B2A,
+ 0x0B30,
+ 0x0B32,
+ 0x0B33,
+ 0x0B36,
+ 0x0B39,
+ 0x0B3D,
+ 0x0B3E,
+ 0x0B40,
+ 0x0B40,
+ 0x0B47,
+ 0x0B48,
+ 0x0B4B,
+ 0x0B4C,
+ 0x0B57,
+ 0x0B57,
+ 0x0B5C,
+ 0x0B5D,
+ 0x0B5F,
+ 0x0B61,
+ 0x0B66,
+ 0x0B70,
+ 0x0B83,
+ 0x0B83,
+ 0x0B85,
+ 0x0B8A,
+ 0x0B8E,
+ 0x0B90,
+ 0x0B92,
+ 0x0B95,
+ 0x0B99,
+ 0x0B9A,
+ 0x0B9C,
+ 0x0B9C,
+ 0x0B9E,
+ 0x0B9F,
+ 0x0BA3,
+ 0x0BA4,
+ 0x0BA8,
+ 0x0BAA,
+ 0x0BAE,
+ 0x0BB5,
+ 0x0BB7,
+ 0x0BB9,
+ 0x0BBE,
+ 0x0BBF,
+ 0x0BC1,
+ 0x0BC2,
+ 0x0BC6,
+ 0x0BC8,
+ 0x0BCA,
+ 0x0BCC,
+ 0x0BD7,
+ 0x0BD7,
+ 0x0BE7,
+ 0x0BF2,
+ 0x0C01,
+ 0x0C03,
+ 0x0C05,
+ 0x0C0C,
+ 0x0C0E,
+ 0x0C10,
+ 0x0C12,
+ 0x0C28,
+ 0x0C2A,
+ 0x0C33,
+ 0x0C35,
+ 0x0C39,
+ 0x0C41,
+ 0x0C44,
+ 0x0C60,
+ 0x0C61,
+ 0x0C66,
+ 0x0C6F,
+ 0x0C82,
+ 0x0C83,
+ 0x0C85,
+ 0x0C8C,
+ 0x0C8E,
+ 0x0C90,
+ 0x0C92,
+ 0x0CA8,
+ 0x0CAA,
+ 0x0CB3,
+ 0x0CB5,
+ 0x0CB9,
+ 0x0CBE,
+ 0x0CBE,
+ 0x0CC0,
+ 0x0CC4,
+ 0x0CC7,
+ 0x0CC8,
+ 0x0CCA,
+ 0x0CCB,
+ 0x0CD5,
+ 0x0CD6,
+ 0x0CDE,
+ 0x0CDE,
+ 0x0CE0,
+ 0x0CE1,
+ 0x0CE6,
+ 0x0CEF,
+ 0x0D02,
+ 0x0D03,
+ 0x0D05,
+ 0x0D0C,
+ 0x0D0E,
+ 0x0D10,
+ 0x0D12,
+ 0x0D28,
+ 0x0D2A,
+ 0x0D39,
+ 0x0D3E,
+ 0x0D40,
+ 0x0D46,
+ 0x0D48,
+ 0x0D4A,
+ 0x0D4C,
+ 0x0D57,
+ 0x0D57,
+ 0x0D60,
+ 0x0D61,
+ 0x0D66,
+ 0x0D6F,
+ 0x0D82,
+ 0x0D83,
+ 0x0D85,
+ 0x0D96,
+ 0x0D9A,
+ 0x0DB1,
+ 0x0DB3,
+ 0x0DBB,
+ 0x0DBD,
+ 0x0DBD,
+ 0x0DC0,
+ 0x0DC6,
+ 0x0DCF,
+ 0x0DD1,
+ 0x0DD8,
+ 0x0DDF,
+ 0x0DF2,
+ 0x0DF4,
+ 0x0E01,
+ 0x0E30,
+ 0x0E32,
+ 0x0E33,
+ 0x0E40,
+ 0x0E46,
+ 0x0E4F,
+ 0x0E5B,
+ 0x0E81,
+ 0x0E82,
+ 0x0E84,
+ 0x0E84,
+ 0x0E87,
+ 0x0E88,
+ 0x0E8A,
+ 0x0E8A,
+ 0x0E8D,
+ 0x0E8D,
+ 0x0E94,
+ 0x0E97,
+ 0x0E99,
+ 0x0E9F,
+ 0x0EA1,
+ 0x0EA3,
+ 0x0EA5,
+ 0x0EA5,
+ 0x0EA7,
+ 0x0EA7,
+ 0x0EAA,
+ 0x0EAB,
+ 0x0EAD,
+ 0x0EB0,
+ 0x0EB2,
+ 0x0EB3,
+ 0x0EBD,
+ 0x0EBD,
+ 0x0EC0,
+ 0x0EC4,
+ 0x0EC6,
+ 0x0EC6,
+ 0x0ED0,
+ 0x0ED9,
+ 0x0EDC,
+ 0x0EDD,
+ 0x0F00,
+ 0x0F17,
+ 0x0F1A,
+ 0x0F34,
+ 0x0F36,
+ 0x0F36,
+ 0x0F38,
+ 0x0F38,
+ 0x0F3E,
+ 0x0F47,
+ 0x0F49,
+ 0x0F6A,
+ 0x0F7F,
+ 0x0F7F,
+ 0x0F85,
+ 0x0F85,
+ 0x0F88,
+ 0x0F8B,
+ 0x0FBE,
+ 0x0FC5,
+ 0x0FC7,
+ 0x0FCC,
+ 0x0FCF,
+ 0x0FCF,
+ 0x1000,
+ 0x1021,
+ 0x1023,
+ 0x1027,
+ 0x1029,
+ 0x102A,
+ 0x102C,
+ 0x102C,
+ 0x1031,
+ 0x1031,
+ 0x1038,
+ 0x1038,
+ 0x1040,
+ 0x1057,
+ 0x10A0,
+ 0x10C5,
+ 0x10D0,
+ 0x10F8,
+ 0x10FB,
+ 0x10FB,
+ 0x1100,
+ 0x1159,
+ 0x115F,
+ 0x11A2,
+ 0x11A8,
+ 0x11F9,
+ 0x1200,
+ 0x1206,
+ 0x1208,
+ 0x1246,
+ 0x1248,
+ 0x1248,
+ 0x124A,
+ 0x124D,
+ 0x1250,
+ 0x1256,
+ 0x1258,
+ 0x1258,
+ 0x125A,
+ 0x125D,
+ 0x1260,
+ 0x1286,
+ 0x1288,
+ 0x1288,
+ 0x128A,
+ 0x128D,
+ 0x1290,
+ 0x12AE,
+ 0x12B0,
+ 0x12B0,
+ 0x12B2,
+ 0x12B5,
+ 0x12B8,
+ 0x12BE,
+ 0x12C0,
+ 0x12C0,
+ 0x12C2,
+ 0x12C5,
+ 0x12C8,
+ 0x12CE,
+ 0x12D0,
+ 0x12D6,
+ 0x12D8,
+ 0x12EE,
+ 0x12F0,
+ 0x130E,
+ 0x1310,
+ 0x1310,
+ 0x1312,
+ 0x1315,
+ 0x1318,
+ 0x131E,
+ 0x1320,
+ 0x1346,
+ 0x1348,
+ 0x135A,
+ 0x1361,
+ 0x137C,
+ 0x13A0,
+ 0x13F4,
+ 0x1401,
+ 0x1676,
+ 0x1681,
+ 0x169A,
+ 0x16A0,
+ 0x16F0,
+ 0x1700,
+ 0x170C,
+ 0x170E,
+ 0x1711,
+ 0x1720,
+ 0x1731,
+ 0x1735,
+ 0x1736,
+ 0x1740,
+ 0x1751,
+ 0x1760,
+ 0x176C,
+ 0x176E,
+ 0x1770,
+ 0x1780,
+ 0x17B6,
+ 0x17BE,
+ 0x17C5,
+ 0x17C7,
+ 0x17C8,
+ 0x17D4,
+ 0x17DA,
+ 0x17DC,
+ 0x17DC,
+ 0x17E0,
+ 0x17E9,
+ 0x1810,
+ 0x1819,
+ 0x1820,
+ 0x1877,
+ 0x1880,
+ 0x18A8,
+ 0x1E00,
+ 0x1E9B,
+ 0x1EA0,
+ 0x1EF9,
+ 0x1F00,
+ 0x1F15,
+ 0x1F18,
+ 0x1F1D,
+ 0x1F20,
+ 0x1F45,
+ 0x1F48,
+ 0x1F4D,
+ 0x1F50,
+ 0x1F57,
+ 0x1F59,
+ 0x1F59,
+ 0x1F5B,
+ 0x1F5B,
+ 0x1F5D,
+ 0x1F5D,
+ 0x1F5F,
+ 0x1F7D,
+ 0x1F80,
+ 0x1FB4,
+ 0x1FB6,
+ 0x1FBC,
+ 0x1FBE,
+ 0x1FBE,
+ 0x1FC2,
+ 0x1FC4,
+ 0x1FC6,
+ 0x1FCC,
+ 0x1FD0,
+ 0x1FD3,
+ 0x1FD6,
+ 0x1FDB,
+ 0x1FE0,
+ 0x1FEC,
+ 0x1FF2,
+ 0x1FF4,
+ 0x1FF6,
+ 0x1FFC,
+ 0x200E,
+ 0x200E,
+ 0x2071,
+ 0x2071,
+ 0x207F,
+ 0x207F,
+ 0x2102,
+ 0x2102,
+ 0x2107,
+ 0x2107,
+ 0x210A,
+ 0x2113,
+ 0x2115,
+ 0x2115,
+ 0x2119,
+ 0x211D,
+ 0x2124,
+ 0x2124,
+ 0x2126,
+ 0x2126,
+ 0x2128,
+ 0x2128,
+ 0x212A,
+ 0x212D,
+ 0x212F,
+ 0x2131,
+ 0x2133,
+ 0x2139,
+ 0x213D,
+ 0x213F,
+ 0x2145,
+ 0x2149,
+ 0x2160,
+ 0x2183,
+ 0x2336,
+ 0x237A,
+ 0x2395,
+ 0x2395,
+ 0x249C,
+ 0x24E9,
+ 0x3005,
+ 0x3007,
+ 0x3021,
+ 0x3029,
+ 0x3031,
+ 0x3035,
+ 0x3038,
+ 0x303C,
+ 0x3041,
+ 0x3096,
+ 0x309D,
+ 0x309F,
+ 0x30A1,
+ 0x30FA,
+ 0x30FC,
+ 0x30FF,
+ 0x3105,
+ 0x312C,
+ 0x3131,
+ 0x318E,
+ 0x3190,
+ 0x31B7,
+ 0x31F0,
+ 0x321C,
+ 0x3220,
+ 0x3243,
+ 0x3260,
+ 0x327B,
+ 0x327F,
+ 0x32B0,
+ 0x32C0,
+ 0x32CB,
+ 0x32D0,
+ 0x32FE,
+ 0x3300,
+ 0x3376,
+ 0x337B,
+ 0x33DD,
+ 0x33E0,
+ 0x33FE,
+ 0x3400,
+ 0x4DB5,
+ 0x4E00,
+ 0x9FA5,
+ 0xA000,
+ 0xA48C,
+ 0xAC00,
+ 0xD7A3,
+ 0xD800,
+ 0xFA2D,
+ 0xFA30,
+ 0xFA6A,
+ 0xFB00,
+ 0xFB06,
+ 0xFB13,
+ 0xFB17,
+ 0xFF21,
+ 0xFF3A,
+ 0xFF41,
+ 0xFF5A,
+ 0xFF66,
+ 0xFFBE,
+ 0xFFC2,
+ 0xFFC7,
+ 0xFFCA,
+ 0xFFCF,
+ 0xFFD2,
+ 0xFFD7,
+ 0xFFDA,
+ 0xFFDC,
+ 0x10300,
+ 0x1031E,
+ 0x10320,
+ 0x10323,
+ 0x10330,
+ 0x1034A,
+ 0x10400,
+ 0x10425,
+ 0x10428,
+ 0x1044D,
+ 0x1D000,
+ 0x1D0F5,
+ 0x1D100,
+ 0x1D126,
+ 0x1D12A,
+ 0x1D166,
+ 0x1D16A,
+ 0x1D172,
+ 0x1D183,
+ 0x1D184,
+ 0x1D18C,
+ 0x1D1A9,
+ 0x1D1AE,
+ 0x1D1DD,
+ 0x1D400,
+ 0x1D454,
+ 0x1D456,
+ 0x1D49C,
+ 0x1D49E,
+ 0x1D49F,
+ 0x1D4A2,
+ 0x1D4A2,
+ 0x1D4A5,
+ 0x1D4A6,
+ 0x1D4A9,
+ 0x1D4AC,
+ 0x1D4AE,
+ 0x1D4B9,
+ 0x1D4BB,
+ 0x1D4BB,
+ 0x1D4BD,
+ 0x1D4C0,
+ 0x1D4C2,
+ 0x1D4C3,
+ 0x1D4C5,
+ 0x1D505,
+ 0x1D507,
+ 0x1D50A,
+ 0x1D50D,
+ 0x1D514,
+ 0x1D516,
+ 0x1D51C,
+ 0x1D51E,
+ 0x1D539,
+ 0x1D53B,
+ 0x1D53E,
+ 0x1D540,
+ 0x1D544,
+ 0x1D546,
+ 0x1D546,
+ 0x1D54A,
+ 0x1D550,
+ 0x1D552,
+ 0x1D6A3,
+ 0x1D6A8,
+ 0x1D7C9,
+ 0x20000,
+ 0x2A6D6,
+ 0x2F800,
+ 0x2FA1D,
+ 0xF0000,
+ 0xFFFFD,
+ 0x100000,
+ 0x10FFFD,});
+
+ /** rfc4013 2.3. Prohibited Output */
+ static final CharClass saslProhibited = CharClass.fromClasses(C12, C21, C22, C3, C4, C5, C6, C7, C8, C9);
+
+ /** A prohibited string has been passed to StringPrep. */
+ public abstract static class StringPrepError extends Exception {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ protected StringPrepError(String message) {
+ super(message);
+ }
+ }
+
+ /** A prohibited character was detected. */
+ @SuppressWarnings({"WeakerAccess", "JavaDoc"})
+ public static class StringPrepProhibitedCharacter extends StringPrepError {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ StringPrepProhibitedCharacter() {
+ super("String contains a prohibited character");
+ }
+
+ protected StringPrepProhibitedCharacter(String s) {
+ super(s);
+ }
+ }
+
+ /** A prohibited unassigned codepoint was detected. */
+ @SuppressWarnings("JavaDoc")
+ public static class StringPrepUnassignedCodepoint extends StringPrepProhibitedCharacter {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ StringPrepUnassignedCodepoint() {
+ super("String contains an unassigned codepoint");
+ }
+ }
+
+ /** RTL verification has failed, according to rfc3454 section 6. */
+ @SuppressWarnings({"unused", "JavaDoc"})
+ public static class StringPrepRTLError extends StringPrepError {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ StringPrepRTLError() {
+ super("Invalid RTL string");
+ }
+ }
+
+ public static class StringPrepRTLErrorBothRALandL extends StringPrepRTLError {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+ }
+
+ public static class StringPrepRTLErrorRALWithoutPrefix extends StringPrepRTLError {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+ }
+
+ public static class StringPrepRTLErrorRALWithoutSuffix extends StringPrepRTLError {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+ }
+
+ /**
+ * Replace each character of {@code s} which is in the {@link CharClass} mapFrom with the string
+ * {@code mapTo}.
+ */
+ static String applyMapTo(String s, CharClass mapFrom, String mapTo) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < s.length();) {
+ int c = Character.codePointAt(s, i);
+ int charCount = Character.charCount(c);
+ if (mapFrom.isCharInClass(c))
+ result.append(mapTo);
+ else
+ result.append(s, i, i + charCount);
+ i += charCount;
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Return the first character index in s which is in {@link CharClass}, or -1 if no character is
+ * in the class.
+ */
+ static int containsCharacterInClass(String s, CharClass charClass) {
+ for (int i = 0; i < s.length();) {
+ int c = Character.codePointAt(s, i);
+ if (charClass.isCharInClass(c))
+ return i;
+
+ i += Character.charCount(c);
+ }
+ return -1;
+ }
+
+ /**
+ * Perform RTL verification according to rfc3454 section 6. On failure, throw a subclass of
+ * {@link StringPrepRTLError}.
+ */
+ protected static void verifyRTL(String s) throws StringPrepRTLError {
+ int containsRAL = containsCharacterInClass(s, D1);
+ if (containsRAL != -1) {
+ // 2) If a string contains any RandALCat character, the string MUST NOT
+ // contain any LCat character.
+ int containsL = containsCharacterInClass(s, D2);
+ if (containsL != -1)
+ throw new StringPrepRTLErrorBothRALandL();
+ // 3) If a string contains any RandALCat character, a RandALCat
+ // character MUST be the first character of the string
+ if (containsRAL != 0)
+ throw new StringPrepRTLErrorRALWithoutPrefix();
+
+ // ... and a RandALCat character MUST be the last character of the string.
+ if (!D1.isCharInClass(s.charAt(s.length() - 1)))
+ throw new StringPrepRTLErrorRALWithoutSuffix();
+ }
+ }
+
+ /** Apply SASLPrep and return the result. {@code} is treated as a stored string. */
+ public static String prepAsStoredString(String s) throws StringPrepError {
+ s = prepAsQueryString(s);
+
+ // rfc3454: 7. Unassigned Code Points in Stringprep Profiles
+ // Stored strings using the profile MUST NOT contain any unassigned code points.
+ // rfc4013: 2.5. Unassigned Code Points
+ // This profile specifies the [StringPrep, A.1] table as its list of unassigned
+ // code points.
+ int containsUnassignedCodepoint = containsCharacterInClass(s, A1);
+ if (containsUnassignedCodepoint != -1)
+ throw new StringPrepUnassignedCodepoint();
+ return s;
+ }
+
+ /** Apply SASLPrep and return the result. {@code} is treated as a query string. */
+ public static String prepAsQueryString(String s) throws StringPrepError {
+ // 1) Map
+ // rfc4013: 2.1. Mapping
+ // Note that applying the mapping this way works here because we only
+ // map to nothing or space. A StringPrep mapping that maps strings to
+ // another string (eg. case folding) can't be applied sequentially like
+ // this.
+ s = applyMapTo(s, B1, "");
+ s = applyMapTo(s, C12, " ");
+
+ // 2) Normalize
+ // rfc4013: 2.2. Normalization
+ s = Normalizer.normalize(s, Normalizer.Form.NFKC);
+
+ // 3) Prohibit
+ int idx = containsCharacterInClass(s, saslProhibited);
+ if (idx != -1)
+ throw new StringPrepProhibitedCharacter();
+
+ // 4) Check bidi
+ verifyRTL(s);
+
+ return s;
+ }
+
+ public static boolean isContainingProhibitedCharacters(String s) {
+ int idx = containsCharacterInClass(s, saslProhibited);
+ return idx != -1;
+ }
+}
diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java
new file mode 100644
index 0000000..4f6104d
--- /dev/null
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 Ognyan Bankov
+ * <p>
+ * All rights reserved. Licensed 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.spi.core.security.scram;
+
+/**
+ * Wrapper for user data needed for the SCRAM authentication
+ */
+public class UserData {
+ /**
+ * Salt
+ */
+ public final String salt;
+ /**
+ * Iterations used to salt the password
+ */
+ public final int iterations;
+ /**
+ * Server key
+ */
+ public final String serverKey;
+ /**
+ * Stored key
+ */
+ public final String storedKey;
+
+ /**
+ * Creates new UserData
+ * @param salt Salt
+ * @param iterations Iterations for salting
+ * @param serverKey Server key
+ * @param storedKey Stored key
+ */
+ public UserData(String salt, int iterations, String serverKey, String storedKey) {
+ this.salt = salt;
+ this.iterations = iterations;
+ this.serverKey = serverKey;
+ this.storedKey = storedKey;
+ }
+}
diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModuleTest.java
index d8039a9..7a7a2e2 100644
--- a/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModuleTest.java
+++ b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/Krb5LoginModuleTest.java
@@ -16,17 +16,18 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
-import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
-import java.io.IOException;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import org.junit.Test;
public class Krb5LoginModuleTest {
@@ -52,7 +53,7 @@ public class Krb5LoginModuleTest {
underTest.initialize(subject, new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
- ((Krb5Callback) callbacks[0]).setPeerPrincipal(new UserPrincipal("A"));
+ ((PrincipalsCallback) callbacks[0]).setPeerPrincipals(new Principal[] {new UserPrincipal("A")});
}
}, null, null);
diff --git a/examples/protocols/amqp/pom.xml b/examples/protocols/amqp/pom.xml
index 83d4ca5..b9b1bbe 100644
--- a/examples/protocols/amqp/pom.xml
+++ b/examples/protocols/amqp/pom.xml
@@ -51,6 +51,7 @@ under the License.
<module>proton-clustered-cpp</module>
<module>queue</module>
<module>proton-ruby</module>
+ <module>sasl-scram</module>
</modules>
</profile>
<profile>
diff --git a/examples/protocols/amqp/sasl-scram/pom.xml b/examples/protocols/amqp/sasl-scram/pom.xml
new file mode 100644
index 0000000..a7b34e8
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/pom.xml
@@ -0,0 +1,43 @@
+<?xml version='1.0'?>
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.activemq.examples.amqp</groupId>
+ <artifactId>amqp</artifactId>
+ <version>2.18.0-SNAPSHOT</version>
+ </parent>
+ <properties>
+ <activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
+ </properties>
+
+ <artifactId>sasl-scram</artifactId>
+ <packaging>pom</packaging>
+ <name>ActiveMQ Artemis SASL-SCRAM Example</name>
+
+ <modules>
+ <module>sasl-client</module>
+ <module>sasl-server</module>
+ </modules>
+</project>
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml
new file mode 100644
index 0000000..c05fde8
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml
@@ -0,0 +1,49 @@
+<?xml version='1.0'?>
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.activemq.examples.amqp</groupId>
+ <artifactId>sasl-scram</artifactId>
+ <version>2.18.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>sasl-scram-client</artifactId>
+ <packaging>jar</packaging>
+ <name>ActiveMQ Artemis SASL-SCRAM-Client Example</name>
+
+ <properties>
+ <activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
+ <artemis-version>${project.version}</artemis-version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.qpid</groupId>
+ <artifactId>qpid-jms-client</artifactId>
+ <version>${qpid.jms.version}</version>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-client/readme.md b/examples/protocols/amqp/sasl-scram/sasl-client/readme.md
new file mode 100644
index 0000000..50fe5d7
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-client/readme.md
@@ -0,0 +1,3 @@
+# Artemis SASL-SCRAM Server and Client Example
+
+demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis
diff --git a/examples/protocols/amqp/sasl-scram/sasl-client/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java b/examples/protocols/amqp/sasl-scram/sasl-client/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java
new file mode 100644
index 0000000..5e4dd2e
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-client/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java
@@ -0,0 +1,48 @@
+/*
+ * <p>
+ * All rights reserved. Licensed 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.jms.example;
+
+import javax.jms.Connection;
+import javax.jms.ConnectionFactory;
+import javax.jms.JMSException;
+import javax.jms.MessageConsumer;
+import javax.jms.MessageProducer;
+import javax.jms.Queue;
+import javax.jms.Session;
+import javax.jms.TextMessage;
+
+import org.apache.qpid.jms.JmsConnectionFactory;
+
+public class QPIDClient {
+ public static void main(String[] args) throws JMSException {
+ sendReceive("SCRAM-SHA-1", "hello", "ogre1234");
+ sendReceive("SCRAM-SHA-256", "test", "test");
+ }
+
+ private static void sendReceive(String method, String username, String password) throws JMSException {
+ ConnectionFactory connectionFactory =
+ new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
+ try (Connection connection = connectionFactory.createConnection(username, password)) {
+ Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ Queue queue = session.createQueue("exampleQueue");
+ MessageProducer sender = session.createProducer(queue);
+ sender.send(session.createTextMessage("Hello " + method));
+ connection.start();
+ MessageConsumer consumer = session.createConsumer(queue);
+ TextMessage m = (TextMessage) consumer.receive(5000);
+ System.out.println("message = " + m.getText());
+ }
+ }
+}
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml
new file mode 100644
index 0000000..e1683ab
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml
@@ -0,0 +1,54 @@
+<?xml version='1.0'?>
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.activemq.examples.amqp</groupId>
+ <artifactId>sasl-scram</artifactId>
+ <version>2.18.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>sasl-scram-server</artifactId>
+ <packaging>jar</packaging>
+ <name>ActiveMQ Artemis SASL-SCRAM-Server Example</name>
+
+ <properties>
+ <activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
+ <artemis-version>${project.version}</artemis-version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.activemq</groupId>
+ <artifactId>artemis-server</artifactId>
+ <version>${artemis-version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.activemq</groupId>
+ <artifactId>artemis-amqp-protocol</artifactId>
+ <version>${artemis-version}</version>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/readme.md b/examples/protocols/amqp/sasl-scram/sasl-server/readme.md
new file mode 100644
index 0000000..1cc11ed
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/readme.md
@@ -0,0 +1,3 @@
+# Artemis SASL-SCRAM Server and Client Example
+
+demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java
new file mode 100644
index 0000000..4bb92d3
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java
@@ -0,0 +1,35 @@
+/*
+ * <p>
+ * All rights reserved. Licensed 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.jms.example;
+
+import java.io.File;
+
+import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
+import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
+
+public class TestServer {
+ public static void main(String[] args) throws Exception {
+ File configFolder = new File(args.length > 0 ? args[0] : "src/main/resources/").getAbsoluteFile();
+ System.setProperty("java.security.auth.login.config", new File(configFolder, "login.conf").getAbsolutePath());
+ EmbeddedActiveMQ embedded = new EmbeddedActiveMQ();
+ embedded.setSecurityManager(new ActiveMQJAASSecurityManager("artemis"));
+ embedded.setConfigResourcePath(new File(configFolder, "broker.xml").getAbsoluteFile().toURI().toASCIIString());
+ embedded.start();
+ while (true) {
+ // intentional empty
+ }
+
+ }
+}
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-roles.properties b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-roles.properties
new file mode 100644
index 0000000..083c1e8
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-roles.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+user=hello,test
+admin=test
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-users.properties b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-users.properties
new file mode 100644
index 0000000..4846235
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-users.properties
@@ -0,0 +1,24 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+##
+
+# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
+test|SHA512 = ENC(7TilOEFipzE4KNkDUTlfnuMkYE1yveyXmK6iBx8/fnE=:4096:yPl/n8eZQEyVmkhuYvrgZCchEpO+a9QiGLXwJfqBWOIfTxMX5TkoHp5eYGABc68cUvoynqCnoqRLDPac+H1urg==:eX5X39hbChbXz00TCkMpmsHqsJTiMGCwamty6yjUS0M+HoE/SLtd2MYY1Shyn+5mu30qFsbXz0WlRA+dZ3Lv3A==)
+test|SHA256 = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)
+test|SHA1 = ENC(ehArM+Qzko2eua0hMq0o+NQ9BaTTf4q8xY0tzfy2Zvw=:4096:LvpLr4ezL4ICxeiXAkXEVH9EhO0=:gLELi8NpLVorxXbPIIbVZF/oqh8=)
+# Example for a plain username/password, don't use this on public servers!
+hello = ogre1234
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml
new file mode 100644
index 0000000..a0252ac
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml
@@ -0,0 +1,50 @@
+<?xml version='1.0'?>
+<!--
+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.
+-->
+<configuration
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="urn:activemq"
+ xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
+ <core xmlns="urn:activemq:core">
+
+ <persistence-enabled>false</persistence-enabled>
+
+ <security-enabled>true</security-enabled>
+
+ <acceptors>
+ <acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
+ </acceptor>
+ </acceptors>
+ <security-settings>
+ <security-setting match="#">
+ <permission type="createAddress" roles="user" />
+ <permission type="createDurableQueue"
+ roles="user" />
+ <permission type="deleteDurableQueue"
+ roles="user" />
+ <permission type="createNonDurableQueue"
+ roles="user" />
+ <permission type="deleteNonDurableQueue"
+ roles="user" />
+ <permission type="consume" roles="user" />
+ <permission type="send" roles="user" />
+ </security-setting>
+ </security-settings>
+ </core>
+</configuration>
\ No newline at end of file
diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf
new file mode 100644
index 0000000..7fad76f
--- /dev/null
+++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+amqp-sasl-scram {
+ org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
+ debug=false
+ org.apache.activemq.jaas.properties.user="artemis-users.properties"
+ org.apache.activemq.jaas.properties.role="artemis-roles.properties";
+};
+
+
+artemis {
+ org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
+ ;
+};
\ No newline at end of file
diff --git a/tests/integration-tests/.gitignore b/tests/integration-tests/.gitignore
new file mode 100644
index 0000000..00d2ab7
--- /dev/null
+++ b/tests/integration-tests/.gitignore
@@ -0,0 +1,2 @@
+/.apt_generated/
+/.apt_generated_tests/
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPConnectSaslTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPConnectSaslTest.java
index 43c938d..0a37e89 100644
--- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPConnectSaslTest.java
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPConnectSaslTest.java
@@ -17,13 +17,24 @@
package org.apache.activemq.artemis.tests.integration.amqp.connect;
import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
+import javax.crypto.Mac;
+import javax.security.auth.login.LoginException;
+
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
+import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
+import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
+import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
+import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
+import org.apache.activemq.artemis.spi.core.security.scram.UserData;
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
import org.apache.qpid.proton.engine.Sasl;
import org.apache.qpid.proton.engine.Sasl.SaslOutcome;
@@ -107,7 +118,7 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
// No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered
AMQPBrokerConnectConfiguration amqpConnection =
- new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
+ new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
server.getConfiguration().addAMQPConnection(amqpConnection);
@@ -135,7 +146,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
});
// User and pass are given, it will select PLAIN
- AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
+ AMQPBrokerConnectConfiguration amqpConnection =
+ new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser(USER);
amqpConnection.setPassword(PASSWD);
@@ -151,6 +163,36 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
assertArrayEquals(expectedPlainInitialResponse(USER, PASSWD), authenticator.getInitialResponse());
}
+ @Test(timeout = 200000)
+ public void testConnectsWithSCRAM() throws Exception {
+ CountDownLatch serverConnectionOpen = new CountDownLatch(1);
+ SCRAMTestAuthenticator authenticator = new SCRAMTestAuthenticator(SCRAM.SHA512);
+
+ mockServer = new MockServer(vertx, () -> authenticator, serverConnection -> {
+ serverConnection.openHandler(serverSender -> {
+ serverConnectionOpen.countDown();
+ serverConnection.closeHandler(x -> serverConnection.close());
+ serverConnection.open();
+ });
+ });
+
+ AMQPBrokerConnectConfiguration amqpConnection =
+ new AMQPBrokerConnectConfiguration("testSScramConnect", "tcp://localhost:" + mockServer.actualPort());
+ amqpConnection.setReconnectAttempts(0);// No reconnects
+ amqpConnection.setUser(USER);
+ amqpConnection.setPassword(PASSWD);
+
+ server.getConfiguration().addAMQPConnection(amqpConnection);
+
+ server.start();
+
+ boolean awaitConnectionOpen = serverConnectionOpen.await(10, TimeUnit.SECONDS);
+ assertTrue("Broker did not open connection in alotted time", awaitConnectionOpen);
+ assertEquals(SCRAM.SHA512.getName(), authenticator.chosenMech);
+ assertTrue(authenticator.succeeded());
+
+ }
+
@Test(timeout = 20000)
public void testConnectsWithExternal() throws Exception {
doConnectWithExternalTestImpl(true);
@@ -161,10 +203,13 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
doConnectWithExternalTestImpl(false);
}
- private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException, InterruptedException, Exception {
+ private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException,
+ InterruptedException, Exception {
CountDownLatch serverConnectionOpen = new CountDownLatch(1);
- // The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker only selects it when it actually
- // has a client-cert. Real servers shouldnt actually offer the mechanism to a client that didnt have to provide a cert.
+ // The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker
+ // only selects it when it actually
+ // has a client-cert. Real servers shouldnt actually offer the mechanism to a client that
+ // didnt have to provide a cert.
TestAuthenticator authenticator = new TestAuthenticator(true, EXTERNAL, PLAIN);
final String keyStorePath = this.getClass().getClassLoader().getResource(SERVER_KEYSTORE_NAME).getFile();
@@ -191,14 +236,17 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
});
String amqpServerConnectionURI = "tcp://localhost:" + mockServer.actualPort() +
- "?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
+ "?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
if (requireClientCert) {
- amqpServerConnectionURI += ";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
+ amqpServerConnectionURI +=
+ ";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
}
- AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
+ AMQPBrokerConnectConfiguration amqpConnection =
+ new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
amqpConnection.setReconnectAttempts(0);// No reconnects
- amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate is provided, but will otherwise.
+ amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate
+ // is provided, but will otherwise.
amqpConnection.setPassword(PASSWD);
server.getConfiguration().addAMQPConnection(amqpConnection);
@@ -236,8 +284,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
private static final class TestAuthenticator implements ProtonSaslAuthenticator {
private Sasl sasl;
- private boolean succeed;
- private String[] offeredMechs;
+ private final boolean succeed;
+ private final String[] offeredMechs;
String chosenMech = null;
byte[] initialResponse = null;
boolean done = false;
@@ -268,7 +316,6 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
initialResponse = new byte[sasl.pending()];
sasl.recv(initialResponse, 0, initialResponse.length);
-
if (succeed) {
sasl.done(SaslOutcome.PN_SASL_OK);
} else {
@@ -296,4 +343,103 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
}
}
+ private static final class SCRAMTestAuthenticator implements ProtonSaslAuthenticator {
+
+ private final SCRAM mech;
+ private Sasl sasl;
+ private TestSCRAMServerSASL serverSASL;
+ private String chosenMech;
+
+ SCRAMTestAuthenticator(SCRAM mech) {
+ this.mech = mech;
+ }
+
+ @Override
+ public void init(NetSocket socket, ProtonConnection protonConnection, Transport transport) {
+ this.sasl = transport.sasl();
+ sasl.server();
+ sasl.allowSkip(false);
+ sasl.setMechanisms(mech.getName(), PLAIN, ANONYMOUS);
+ try {
+ serverSASL = new TestSCRAMServerSASL(mech);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+
+ }
+
+ @Override
+ public void process(Handler<Boolean> completionHandler) {
+ String[] remoteMechanisms = sasl.getRemoteMechanisms();
+ int pending = sasl.pending();
+ if (remoteMechanisms.length == 0 || pending == 0) {
+ completionHandler.handle(false);
+ return;
+ }
+ chosenMech = remoteMechanisms[0];
+ byte[] msg = new byte[pending];
+ sasl.recv(msg, 0, msg.length);
+ byte[] result = serverSASL.processSASL(msg);
+ if (result != null) {
+ sasl.send(result, 0, result.length);
+ }
+ boolean ended = serverSASL.isEnded();
+ if (ended) {
+ if (succeeded()) {
+ sasl.done(SaslOutcome.PN_SASL_OK);
+ } else {
+ sasl.done(SaslOutcome.PN_SASL_AUTH);
+ }
+ completionHandler.handle(true);
+ } else {
+ completionHandler.handle(false);
+ }
+ }
+
+ @Override
+ public boolean succeeded() {
+ SASLResult result = serverSASL.result();
+ return result != null && result.isSuccess() && serverSASL.e == null;
+ }
+
+ }
+
+ private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
+
+ private Exception e;
+
+ TestSCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
+ super(mechanism);
+ }
+
+ @Override
+ public void done() {
+ // nothing to do
+ }
+
+ @Override
+ protected UserData aquireUserData(String userName) throws LoginException {
+ if (!USER.equals(userName)) {
+ throw new LoginException("invalid username");
+ }
+ byte[] salt = new byte[32];
+ new SecureRandom().nextBytes(salt);
+ try {
+ MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
+ Mac hmac = Mac.getInstance(mechanism.getHmac());
+ ScramUtils.NewPasswordStringData data =
+ ScramUtils.byteArrayToStringData(ScramUtils.newPassword(PASSWD, salt, 4096, digest, hmac));
+ return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
+ } catch (Exception e) {
+ throw new LoginException(e.getMessage());
+ }
+ }
+
+ @Override
+ protected void failed(Exception e) {
+ this.e = e;
+ }
+
+ }
+
}
diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java
new file mode 100644
index 0000000..76b1d4f
--- /dev/null
+++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java
@@ -0,0 +1,117 @@
+/**
+ * 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.tests.integration.amqp.sasl;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+
+import javax.jms.Connection;
+import javax.jms.ConnectionFactory;
+import javax.jms.JMSException;
+import javax.jms.MessageConsumer;
+import javax.jms.MessageProducer;
+import javax.jms.Queue;
+import javax.jms.Session;
+import javax.jms.TextMessage;
+
+import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
+import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
+import org.apache.qpid.jms.JmsConnectionFactory;
+import org.apache.qpid.jms.exceptions.JMSSecuritySaslException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * This test SASL-SCRAM Support
+ */
+public class SaslScramTest {
+
+ private static EmbeddedActiveMQ BROKER;
+
+ @BeforeClass
+ public static void startBroker() throws Exception {
+ String loginConfPath = new File(SaslScramTest.class.getResource("/login.config").toURI()).getAbsolutePath();
+ System.out.println(loginConfPath);
+ System.setProperty("java.security.auth.login.config", loginConfPath);
+ BROKER = new EmbeddedActiveMQ();
+ BROKER.setConfigResourcePath(SaslScramTest.class.getResource("/broker-saslscram.xml").toExternalForm());
+ BROKER.setSecurityManager(new ActiveMQJAASSecurityManager("artemis-sasl-scram"));
+ BROKER.start();
+ }
+
+ @AfterClass
+ public static void shutdownBroker() throws Exception {
+ BROKER.stop();
+ }
+
+ /**
+ * Checks if a user with plain text password can login using all mechanisms
+ * @throws JMSException should not happen
+ */
+ @Test
+ public void testUnencryptedWorksWithAllMechanism() throws JMSException {
+ sendRcv("SCRAM-SHA-1", "hello", "ogre1234");
+ sendRcv("SCRAM-SHA-256", "hello", "ogre1234");
+ }
+
+ /**
+ * Checks that a user that has encrypted passwords for all mechanism can login with any of them
+ * @throws JMSException should not happen
+ */
+ @Test
+ public void testEncryptedWorksWithAllMechanism() throws JMSException {
+ sendRcv("SCRAM-SHA-1", "multi", "worksforall");
+ sendRcv("SCRAM-SHA-256", "multi", "worksforall");
+ }
+
+ /**
+ * Checks that a user that is only stored with one explicit mechanism can't use another mechanism
+ * @throws JMSException is expected
+ */
+ @Test(expected = JMSSecuritySaslException.class)
+ public void testEncryptedWorksOnlyWithMechanism() throws JMSException {
+ sendRcv("SCRAM-SHA-1", "test", "test");
+ }
+
+ /**
+ * Checks that a user that is only stored with one explicit mechanism can login with this
+ * mechanism
+ * @throws JMSException should not happen
+ */
+ @Test
+ public void testEncryptedWorksWithMechanism() throws JMSException {
+ sendRcv("SCRAM-SHA-256", "test", "test");
+ }
+
+ private void sendRcv(String method, String username, String password) throws JMSException {
+ ConnectionFactory connectionFactory =
+ new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
+ try (Connection connection = connectionFactory.createConnection(username, password)) {
+ Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ Queue queue = session.createQueue("exampleQueue");
+ MessageProducer sender = session.createProducer(queue);
+ String text = "Hello " + method;
+ sender.send(session.createTextMessage(text));
+ connection.start();
+ MessageConsumer consumer = session.createConsumer(queue);
+ TextMessage m = (TextMessage) consumer.receive(5000);
+ assertEquals(text, m.getText());
+ }
+ }
+}
diff --git a/tests/integration-tests/src/test/resources/artemis-scram-roles.properties b/tests/integration-tests/src/test/resources/artemis-scram-roles.properties
new file mode 100644
index 0000000..ccff1c4
--- /dev/null
+++ b/tests/integration-tests/src/test/resources/artemis-scram-roles.properties
@@ -0,0 +1,18 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+user=hello,test,multi
+admin=multi
\ No newline at end of file
diff --git a/tests/integration-tests/src/test/resources/artemis-scram-users.properties b/tests/integration-tests/src/test/resources/artemis-scram-users.properties
new file mode 100644
index 0000000..23a2e36
--- /dev/null
+++ b/tests/integration-tests/src/test/resources/artemis-scram-users.properties
@@ -0,0 +1,26 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
+multi|SHA256 = ENC(o3ljCITL4Cw6pu+fxvz4k68F8jQuZAITcRNpy2THufw=:4096:Niuc0/lWg/YQztHqJCJ5SodyxbWPtGj6zp/HHPqSDBY=:O1YL/w08fvuTvqctbHrr4TxpzKso+NCdqt4Amqp7r0k=)
+multi|SHA1 = ENC(cJyfpU4wgmoSz1GVc39+CooXY2jIv2ILe7486+l9vbg=:4096:sMCix9TeOvmo2eb4xbCjQt4navs=:fSKdLYAgdx36RMjjMSn1dZ7IpY8=)
+
+# Example for a plain username/password, don't use this on public servers!
+hello = ogre1234
+
+# just for unit-test purpose!
+test = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)
diff --git a/tests/integration-tests/src/test/resources/broker-saslscram.xml b/tests/integration-tests/src/test/resources/broker-saslscram.xml
new file mode 100644
index 0000000..a0252ac
--- /dev/null
+++ b/tests/integration-tests/src/test/resources/broker-saslscram.xml
@@ -0,0 +1,50 @@
+<?xml version='1.0'?>
+<!--
+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.
+-->
+<configuration
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="urn:activemq"
+ xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
+ <core xmlns="urn:activemq:core">
+
+ <persistence-enabled>false</persistence-enabled>
+
+ <security-enabled>true</security-enabled>
+
+ <acceptors>
+ <acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
+ </acceptor>
+ </acceptors>
+ <security-settings>
+ <security-setting match="#">
+ <permission type="createAddress" roles="user" />
+ <permission type="createDurableQueue"
+ roles="user" />
+ <permission type="deleteDurableQueue"
+ roles="user" />
+ <permission type="createNonDurableQueue"
+ roles="user" />
+ <permission type="deleteNonDurableQueue"
+ roles="user" />
+ <permission type="consume" roles="user" />
+ <permission type="send" roles="user" />
+ </security-setting>
+ </security-settings>
+ </core>
+</configuration>
\ No newline at end of file
diff --git a/tests/integration-tests/src/test/resources/login.config b/tests/integration-tests/src/test/resources/login.config
index a70836b..6ff980c 100644
--- a/tests/integration-tests/src/test/resources/login.config
+++ b/tests/integration-tests/src/test/resources/login.config
@@ -319,3 +319,15 @@ amqp-jms-client {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true;
};
+
+amqp-sasl-scram {
+ org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
+ debug=false
+ org.apache.activemq.jaas.properties.user="artemis-scram-users.properties"
+ org.apache.activemq.jaas.properties.role="artemis-scram-roles.properties";
+};
+
+artemis-sasl-scram {
+ org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
+ ;
+};