From c7e2bc1ab57d21fe1bec8af8b6bd184bc6e7f5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 06:42:17 +0100 Subject: [PATCH 01/23] ARTEMIS-3106 - Support for SASL-SCRAM adds the implementation necessary to perform SASL-SCRAM authentication with ArtemisMQ --- .../sasl/scram/SCRAMServerSASLFactory.java | 236 ++++++++++++++++++ .../scram/SHA1SCRAMServerSASLFactory.java | 35 +++ .../scram/SHA256SCRAMServerSASLFactory.java | 34 +++ .../scram/SHA512CRAMServerSASLFactory.java | 34 +++ ...temis.protocol.amqp.sasl.ServerSASLFactory | 5 +- .../core/security/jaas/DigestCallback.java | 48 ++++ .../spi/core/security/jaas/HmacCallback.java | 46 ++++ .../security/jaas/SCRAMMechanismCallback.java | 46 ++++ .../jaas/SCRAMPropertiesLoginModule.java | 202 +++++++++++++++ .../spi/core/security/scram/SCRAM.java | 61 +++++ 10 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/SCRAM.java 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 00000000000..526e00e6a13 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java @@ -0,0 +1,236 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.util.Iterator; +import java.util.UUID; + +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 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.SASLResult; +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.ScramException; +import org.apache.activemq.artemis.spi.core.security.scram.UserData; + +/** + * 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 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 manager, Connection connection, + RemotingConnection remotingConnection) { + try { + if (manager instanceof ProtonProtocolManager) { + ScramServerFunctionalityImpl scram = + new ScramServerFunctionalityImpl(scramType.getDigest(), scramType.getHmac(), + UUID.randomUUID().toString()); + String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope(); + return new SCRAMServerSASL(scramType.getName(), scram, loginConfigScope); + } + } catch (NoSuchAlgorithmException e) { + // can't be used then... + } + return null; + } + + private static final class SCRAMServerSASL implements ServerSASL { + + private final String name; + private final ScramServerFunctionality scram; + private SASLResult result; + private final String loginConfigScope; + + public SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope) { + this.name = name; + this.scram = scram; + this.loginConfigScope = loginConfigScope; + } + + @Override + public String getName() { + return name; + } + + @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); + if (userName != null) { + + LoginContext 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(name); + } 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(); + Subject subject = loginContext.getSubject(); + Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); + if (credentials.hasNext()) { + result = new SCRAMSASLResult(userName, scram, subject); + String challenge = scram.prepareFirstMessage(credentials.next()); + return challenge.getBytes(StandardCharsets.US_ASCII); + } + } + break; + } + case PREPARED_FIRST: { + String finalMessage = scram.prepareFinalMessage(message); + if (finalMessage != null) { + return finalMessage.getBytes(StandardCharsets.US_ASCII); + } + break; + } + + default: + result = new SCRAMFailedSASLResult(); + break; + } + } catch (GeneralSecurityException | ScramException | RuntimeException e) { + result = new SCRAMFailedSASLResult(); + } + return null; + } + + @Override + public SASLResult result() { + if (result instanceof SCRAMSASLResult) { + return scram.isEnded() ? result : null; + } + return result; + } + + @Override + public void done() { + } + + } + + private static final class SCRAMSASLResult implements SASLResult { + + private final String userName; + private final ScramServerFunctionality scram; + private final Subject subject; + + public 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"; + } + + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java new file mode 100644 index 00000000000..dbe648cfa51 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java @@ -0,0 +1,35 @@ +/* + * 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 org.apache.activemq.artemis.spi.core.security.scram.SCRAM; + +/** + * provides SASL SCRAM-SHA1 + */ +public class SHA1SCRAMServerSASLFactory extends SCRAMServerSASLFactory { + + public SHA1SCRAMServerSASLFactory() { + super(SCRAM.SHA1); + } + + @Override + public int getPrecedence() { + return 100; + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java new file mode 100644 index 00000000000..57b5c492377 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java @@ -0,0 +1,34 @@ +/* + * 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 org.apache.activemq.artemis.spi.core.security.scram.SCRAM; + +/** + * provides SASL SRAM-SHA256 + */ +public class SHA256SCRAMServerSASLFactory extends SCRAMServerSASLFactory { + + public SHA256SCRAMServerSASLFactory() { + super(SCRAM.SHA256); + } + + @Override + public int getPrecedence() { + return 200; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java new file mode 100644 index 00000000000..643c1de3c9a --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java @@ -0,0 +1,34 @@ +/* + * 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 org.apache.activemq.artemis.spi.core.security.scram.SCRAM; + +/** + * provides SASL SCRAM-SHA512 + */ +public class SHA512CRAMServerSASLFactory extends SCRAMServerSASLFactory { + + public SHA512CRAMServerSASLFactory() { + super(SCRAM.SHA512); + } + + @Override + public int getPrecedence() { + return 500; + } +} 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 eb2c1127c52..10f411622a2 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,7 @@ 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.SHA1SCRAMServerSASLFactory +org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory +org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512CRAMServerSASLFactory \ No newline at end of file diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java new file mode 100644 index 00000000000..8bdb9565ca3 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java @@ -0,0 +1,48 @@ +/* + * 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.security.MessageDigest; + +import javax.security.auth.callback.Callback; + +/** + * Callback to obtain a {@link MessageDigest} for login purpose + * + * + */ +public class DigestCallback implements Callback { + + private MessageDigest digest; + + /** + * set the digest to use + * + * @param digest the digest + */ + public void setDigest(MessageDigest digest) { + this.digest = digest; + } + + /** + * @return the digest or null if not known + */ + public MessageDigest getDigest() { + return digest; + } + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java new file mode 100644 index 00000000000..e1e0b6ee9d9 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java @@ -0,0 +1,46 @@ +/* + * 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 javax.crypto.Mac; +import javax.security.auth.callback.Callback; + +/** + * Callback for obtaining information about a used H{@link Mac} + * + */ +public class HmacCallback implements Callback { + + private Mac hmac; + + /** + * set the Hmac to use + * + * @param hmac + */ + public void setHmac(Mac hmac) { + this.hmac = hmac; + } + + /** + * @return the Hmac or null if non could be obtained + */ + public Mac getHmac() { + return hmac; + } + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java new file mode 100644 index 00000000000..df5eb74ec86 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java @@ -0,0 +1,46 @@ +/* + * 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 javax.security.auth.callback.Callback; + +/** + * callback to obtain the a mechanism used in a SASL-SCRAM authentication + * + */ +public class SCRAMMechanismCallback implements Callback { + + private String name; + + /** + * sets the name of the mechanism + * + * @param name the name of the mechanism + */ + public void setMechanism(String name) { + this.name = name; + + } + + /** + * @return the name of the mechanism + */ + 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 00000000000..9fe0d3eae63 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java @@ -0,0 +1,202 @@ +/* + * 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.FailedLoginException; +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.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 = ":"; + private static final int MIN_ITERATIONS = 4096; + private Subject subject; + private CallbackHandler callbackHandler; + private Properties users; + private Map> roles; + private UserData userData; + private String user; + private final Set principals = new HashSet<>(); + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map 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(new Callback[] { nameCallback }); + user = nameCallback.getName(); + if (user == null) { + throw new FailedLoginException("User is null"); + } + String password = users.getProperty(user); + if (password == null) { + throw new FailedLoginException("User does not exist: " + user); + } + if (PasswordMaskingUtil.isEncMasked(password)) { + String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR); + userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]); + } else { + DigestCallback digestCallback = new DigestCallback(); + HmacCallback hmacCallback = new HmacCallback(); + executeCallbacks(new Callback[] { digestCallback, hmacCallback }); + byte[] salt = generateSalt(); + try { + ScramUtils.NewPasswordStringData data = ScramUtils.byteArrayToStringData(ScramUtils + .newPassword(password, salt, 4096, digestCallback.getDigest(), hmacCallback.getHmac())); + userData = new UserData(data.salt, data.iterations, data.serverKey, data.storedKey); + } catch (ScramException e) { + throw new LoginException(); + } + } + return true; + } + + private static byte[] generateSalt() { + byte[] salt = new byte[24]; + SecureRandom random = new SecureRandom(); + random.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 authenticatedUsers = subject.getPrincipals(UserPrincipal.class); + UserPrincipal principal = new UserPrincipal(user); + principals.add(principal); + authenticatedUsers.add(principal); + for (UserPrincipal userPrincipal : authenticatedUsers) { + Set 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 [] + * @throws GeneralSecurityException if any security mechanism is not available + * on this JVM + * @throws ScramException if invalid data is supplied + */ + public static void main(String[] args) throws GeneralSecurityException, ScramException { + if (args.length < 3) { + System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() + + " []"); + 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]; + String type = args[2]; + SCRAM scram = Arrays.stream(SCRAM.values()).filter(v -> v.getName().equals(type)).findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "unkown type " + type + ", supported ones are " + getSupportedTypes())); + MessageDigest digest = MessageDigest.getInstance(scram.getDigest()); + Mac hmac = Mac.getInstance(scram.getHmac()); + byte[] salt = generateSalt(); + int iterations; + if (args.length > 3) { + iterations = Integer.parseInt(args[3]); + 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)); + System.out.println(username + " = " + PasswordMaskingUtil.wrap( + data.salt + SEPARATOR + data.iterations + SEPARATOR + data.serverKey + SEPARATOR + data.storedKey)); + } + + 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 00000000000..1917cd6614e --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/SCRAM.java @@ -0,0 +1,61 @@ +/* + * 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 { + SHA1, SHA256, SHA512; + + public String getName() { + switch (this) { + case SHA1: + return "SCRAM-SHA-1"; + case SHA256: + return "SCRAM-SHA-256"; + case SHA512: + return "SCRAM-SHA-512"; + } + throw new UnsupportedOperationException(); + } + + public String getDigest() { + switch (this) { + case SHA1: + return "SHA-1"; + case SHA256: + return "SHA-256"; + case SHA512: + return "SHA-512"; + } + throw new UnsupportedOperationException(); + } + + public String getHmac() { + switch (this) { + case SHA1: + return "HmacSHA1"; + case SHA256: + return "HmacSHA256"; + case SHA512: + return "HmacSHA512"; + } + throw new UnsupportedOperationException(); + } +} From 165657e35af1cede0527b57d577438fc04dce9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 06:44:04 +0100 Subject: [PATCH 02/23] ARTEMIS-3106 - Support for SASL-SCRAM adds modified versions of the original "SCRAM SASL authentication for Java" https://github.com/ogrebgr/scram-sasl code --- .../sasl/scram/ScramServerFunctionality.java | 108 +++++++ .../scram/ScramServerFunctionalityImpl.java | 203 ++++++++++++ .../core/security/scram/ScramException.java | 46 +++ .../spi/core/security/scram/ScramUtils.java | 305 ++++++++++++++++++ .../spi/core/security/scram/UserData.java | 56 ++++ 5 files changed, 718 insertions(+) create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionality.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionalityImpl.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramException.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramUtils.java create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java 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 00000000000..e154472e9a3 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionality.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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.security.GeneralSecurityException; +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 GeneralSecurityException if there is an error processing clients + * message + * @throws ScramException + */ + String prepareFinalMessage(String clientFinalMessage) throws GeneralSecurityException, 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 00000000000..44666c3c179 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramServerFunctionalityImpl.java @@ -0,0 +1,203 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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.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 { + 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); + if (!Arrays.equals(storedKeyArr, resultKey)) { + throw new ScramException("Nonce mismatch"); + } + + String result = "v=" + Base64.getEncoder().encodeToString(serverSignature); + mIsSuccessful = true; + return result; + } + + @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-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 00000000000..13c92d381e7 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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; + +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 00000000000..0eda701a488 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/ScramUtils.java @@ -0,0 +1,305 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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; + + +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 + * @throws InvalidKeyException if internal error occur while working with + * SecretKeySpec + */ + 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 + *

+ * 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/UserData.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java new file mode 100644 index 00000000000..f56c6a06775 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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; + + +/** + * 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; + } +} From 50e14131dd6fb80910f7a5879249258ed12d357a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 07:48:29 +0100 Subject: [PATCH 03/23] ARTEMIS-3106 - Support for SASL-SCRAM checkstyle-warnings --- .../sasl/scram/SCRAMServerSASLFactory.java | 4 +- .../scram/SHA1SCRAMServerSASLFactory.java | 14 +- .../scram/SHA256SCRAMServerSASLFactory.java | 14 +- .../scram/SHA512CRAMServerSASLFactory.java | 14 +- .../sasl/scram/ScramServerFunctionality.java | 146 +++-- .../scram/ScramServerFunctionalityImpl.java | 330 ++++++------ .../core/security/jaas/DigestCallback.java | 31 +- .../spi/core/security/jaas/HmacCallback.java | 30 +- .../security/jaas/SCRAMMechanismCallback.java | 30 +- .../jaas/SCRAMPropertiesLoginModule.java | 304 +++++------ .../spi/core/security/scram/SCRAM.java | 73 +-- .../core/security/scram/ScramException.java | 36 +- .../spi/core/security/scram/ScramUtils.java | 505 ++++++++---------- .../spi/core/security/scram/UserData.java | 61 +-- 14 files changed, 765 insertions(+), 827 deletions(-) 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 index 526e00e6a13..b61821569ec 100644 --- 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 @@ -92,7 +92,7 @@ private static final class SCRAMServerSASL implements ServerSASL { private SASLResult result; private final String loginConfigScope; - public SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope) { + SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope) { this.name = name; this.scram = scram; this.loginConfigScope = loginConfigScope; @@ -181,7 +181,7 @@ private static final class SCRAMSASLResult implements SASLResult { private final ScramServerFunctionality scram; private final Subject subject; - public SCRAMSASLResult(String userName, ScramServerFunctionality scram, Subject subject) { + SCRAMSASLResult(String userName, ScramServerFunctionality scram, Subject subject) { this.userName = userName; this.scram = scram; this.subject = subject; diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java index dbe648cfa51..80207897f08 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java @@ -23,13 +23,13 @@ */ public class SHA1SCRAMServerSASLFactory extends SCRAMServerSASLFactory { - public SHA1SCRAMServerSASLFactory() { - super(SCRAM.SHA1); - } + public SHA1SCRAMServerSASLFactory() { + super(SCRAM.SHA1); + } - @Override - public int getPrecedence() { - return 100; - } + @Override + public int getPrecedence() { + return 100; + } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java index 57b5c492377..26154c9787f 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java @@ -23,12 +23,12 @@ */ public class SHA256SCRAMServerSASLFactory extends SCRAMServerSASLFactory { - public SHA256SCRAMServerSASLFactory() { - super(SCRAM.SHA256); - } + public SHA256SCRAMServerSASLFactory() { + super(SCRAM.SHA256); + } - @Override - public int getPrecedence() { - return 200; - } + @Override + public int getPrecedence() { + return 200; + } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java index 643c1de3c9a..21bf98e43ca 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java @@ -23,12 +23,12 @@ */ public class SHA512CRAMServerSASLFactory extends SCRAMServerSASLFactory { - public SHA512CRAMServerSASLFactory() { - super(SCRAM.SHA512); - } + public SHA512CRAMServerSASLFactory() { + super(SCRAM.SHA512); + } - @Override - public int getPrecedence() { - return 500; - } + @Override + public int getPrecedence() { + return 500; + } } 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 index e154472e9a3..12260409ebf 100644 --- 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 @@ -14,10 +14,8 @@ * limitations under the License. */ - package org.apache.activemq.artemis.protocol.amqp.sasl.scram; - import java.security.GeneralSecurityException; import java.security.MessageDigest; @@ -26,83 +24,77 @@ 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 GeneralSecurityException if there is an error processing clients - * message - * @throws ScramException - */ - String prepareFinalMessage(String clientFinalMessage) throws GeneralSecurityException, 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(); + /** + * 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 GeneralSecurityException if there is an error processing clients message + * @throws ScramException + */ + String prepareFinalMessage(String clientFinalMessage) throws GeneralSecurityException, 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 index 44666c3c179..752eb400aa7 100644 --- 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 @@ -34,170 +34,168 @@ * 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 { - 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); - if (!Arrays.equals(storedKeyArr, resultKey)) { - throw new ScramException("Nonce mismatch"); - } - - String result = "v=" + Base64.getEncoder().encodeToString(serverSignature); - mIsSuccessful = true; - return result; - } - - @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); - } - } - } + 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 { + 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); + if (!Arrays.equals(storedKeyArr, resultKey)) { + throw new ScramException("Nonce mismatch"); + } + + String result = "v=" + Base64.getEncoder().encodeToString(serverSignature); + mIsSuccessful = true; + return result; + } + + @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-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java index 8bdb9565ca3..865b2664a51 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/DigestCallback.java @@ -22,27 +22,24 @@ /** * Callback to obtain a {@link MessageDigest} for login purpose - * - * */ public class DigestCallback implements Callback { - private MessageDigest digest; + private MessageDigest digest; - /** - * set the digest to use - * - * @param digest the digest - */ - public void setDigest(MessageDigest digest) { - this.digest = digest; - } + /** + * set the digest to use + * @param digest the digest + */ + public void setDigest(MessageDigest digest) { + this.digest = digest; + } - /** - * @return the digest or null if not known - */ - public MessageDigest getDigest() { - return digest; - } + /** + * @return the digest or null if not known + */ + public MessageDigest getDigest() { + return digest; + } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java index e1e0b6ee9d9..8d8f9ce8883 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/HmacCallback.java @@ -21,26 +21,24 @@ /** * Callback for obtaining information about a used H{@link Mac} - * */ public class HmacCallback implements Callback { - private Mac hmac; + private Mac hmac; - /** - * set the Hmac to use - * - * @param hmac - */ - public void setHmac(Mac hmac) { - this.hmac = hmac; - } + /** + * set the Hmac to use + * @param hmac + */ + public void setHmac(Mac hmac) { + this.hmac = hmac; + } - /** - * @return the Hmac or null if non could be obtained - */ - public Mac getHmac() { - return hmac; - } + /** + * @return the Hmac or null if non could be obtained + */ + public Mac getHmac() { + return hmac; + } } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java index df5eb74ec86..de66108dbfc 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMMechanismCallback.java @@ -20,27 +20,25 @@ /** * callback to obtain the a mechanism used in a SASL-SCRAM authentication - * */ public class SCRAMMechanismCallback implements Callback { - private String name; + private String name; - /** - * sets the name of the mechanism - * - * @param name the name of the mechanism - */ - public void setMechanism(String name) { - this.name = name; + /** + * sets the name of the mechanism + * @param name the name of the mechanism + */ + public void setMechanism(String name) { + this.name = name; - } + } - /** - * @return the name of the mechanism - */ - public String getMechanism() { - return name; - } + /** + * @return the name of the mechanism + */ + 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 index 9fe0d3eae63..6c406400418 100644 --- 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 @@ -43,160 +43,160 @@ 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. + * 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 = ":"; - private static final int MIN_ITERATIONS = 4096; - private Subject subject; - private CallbackHandler callbackHandler; - private Properties users; - private Map> roles; - private UserData userData; - private String user; - private final Set principals = new HashSet<>(); - - @Override - public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, - Map 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(new Callback[] { nameCallback }); - user = nameCallback.getName(); - if (user == null) { - throw new FailedLoginException("User is null"); - } - String password = users.getProperty(user); - if (password == null) { - throw new FailedLoginException("User does not exist: " + user); - } - if (PasswordMaskingUtil.isEncMasked(password)) { - String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR); - userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]); - } else { - DigestCallback digestCallback = new DigestCallback(); - HmacCallback hmacCallback = new HmacCallback(); - executeCallbacks(new Callback[] { digestCallback, hmacCallback }); - byte[] salt = generateSalt(); - try { - ScramUtils.NewPasswordStringData data = ScramUtils.byteArrayToStringData(ScramUtils - .newPassword(password, salt, 4096, digestCallback.getDigest(), hmacCallback.getHmac())); - userData = new UserData(data.salt, data.iterations, data.serverKey, data.storedKey); - } catch (ScramException e) { - throw new LoginException(); - } - } - return true; - } - - private static byte[] generateSalt() { - byte[] salt = new byte[24]; - SecureRandom random = new SecureRandom(); - random.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 authenticatedUsers = subject.getPrincipals(UserPrincipal.class); - UserPrincipal principal = new UserPrincipal(user); - principals.add(principal); - authenticatedUsers.add(principal); - for (UserPrincipal userPrincipal : authenticatedUsers) { - Set 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 [] - * @throws GeneralSecurityException if any security mechanism is not available - * on this JVM - * @throws ScramException if invalid data is supplied - */ - public static void main(String[] args) throws GeneralSecurityException, ScramException { - if (args.length < 3) { - System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() - + " []"); - 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]; - String type = args[2]; - SCRAM scram = Arrays.stream(SCRAM.values()).filter(v -> v.getName().equals(type)).findFirst() - .orElseThrow(() -> new IllegalArgumentException( - "unkown type " + type + ", supported ones are " + getSupportedTypes())); - MessageDigest digest = MessageDigest.getInstance(scram.getDigest()); - Mac hmac = Mac.getInstance(scram.getHmac()); - byte[] salt = generateSalt(); - int iterations; - if (args.length > 3) { - iterations = Integer.parseInt(args[3]); - 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)); - System.out.println(username + " = " + PasswordMaskingUtil.wrap( - data.salt + SEPARATOR + data.iterations + SEPARATOR + data.serverKey + SEPARATOR + data.storedKey)); - } - - private static String getSupportedTypes() { - return String.join(", ", Arrays.stream(SCRAM.values()).map(SCRAM::getName).toArray(String[]::new)); - } + private static final String SEPARATOR = ":"; + private static final int MIN_ITERATIONS = 4096; + private Subject subject; + private CallbackHandler callbackHandler; + private Properties users; + private Map> roles; + private UserData userData; + private String user; + private final Set principals = new HashSet<>(); + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map 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(new Callback[] {nameCallback}); + user = nameCallback.getName(); + if (user == null) { + throw new FailedLoginException("User is null"); + } + String password = users.getProperty(user); + if (password == null) { + throw new FailedLoginException("User does not exist: " + user); + } + if (PasswordMaskingUtil.isEncMasked(password)) { + String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR); + userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]); + } else { + DigestCallback digestCallback = new DigestCallback(); + HmacCallback hmacCallback = new HmacCallback(); + executeCallbacks(new Callback[] {digestCallback, hmacCallback}); + byte[] salt = generateSalt(); + try { + ScramUtils.NewPasswordStringData data = + ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, 4096, + digestCallback.getDigest(), + hmacCallback.getHmac())); + userData = new UserData(data.salt, data.iterations, data.serverKey, data.storedKey); + } catch (ScramException e) { + throw new LoginException(); + } + } + return true; + } + + private static byte[] generateSalt() { + byte[] salt = new byte[24]; + SecureRandom random = new SecureRandom(); + random.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 authenticatedUsers = subject.getPrincipals(UserPrincipal.class); + UserPrincipal principal = new UserPrincipal(user); + principals.add(principal); + authenticatedUsers.add(principal); + for (UserPrincipal userPrincipal : authenticatedUsers) { + Set 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 + */ + public static void main(String[] args) throws GeneralSecurityException, ScramException { + if (args.length < 3) { + System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() + + " []"); + 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]; + String type = args[2]; + SCRAM scram = Arrays.stream(SCRAM.values()) + .filter(v -> v.getName().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("unkown type " + type + + ", supported ones are " + getSupportedTypes())); + MessageDigest digest = MessageDigest.getInstance(scram.getDigest()); + Mac hmac = Mac.getInstance(scram.getHmac()); + byte[] salt = generateSalt(); + int iterations; + if (args.length > 3) { + iterations = Integer.parseInt(args[3]); + 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)); + System.out.println(username + " = " + PasswordMaskingUtil.wrap(data.salt + SEPARATOR + data.iterations + + SEPARATOR + data.serverKey + SEPARATOR + data.storedKey)); + } + + 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 index 1917cd6614e..f994a0d2a09 100644 --- 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 @@ -17,45 +17,46 @@ package org.apache.activemq.artemis.spi.core.security.scram; /** - * Defines sets of known SCRAM types with methods to fetch matching digest and - * hmac names + * Defines sets of known SCRAM types with methods to fetch matching digest and hmac names */ public enum SCRAM { - SHA1, SHA256, SHA512; + SHA1, + SHA256, + SHA512; - public String getName() { - switch (this) { - case SHA1: - return "SCRAM-SHA-1"; - case SHA256: - return "SCRAM-SHA-256"; - case SHA512: - return "SCRAM-SHA-512"; - } - throw new UnsupportedOperationException(); - } + public String getName() { + switch (this) { + case SHA1: + return "SCRAM-SHA-1"; + case SHA256: + return "SCRAM-SHA-256"; + case SHA512: + return "SCRAM-SHA-512"; + } + throw new UnsupportedOperationException(); + } - public String getDigest() { - switch (this) { - case SHA1: - return "SHA-1"; - case SHA256: - return "SHA-256"; - case SHA512: - return "SHA-512"; - } - throw new UnsupportedOperationException(); - } + public String getDigest() { + switch (this) { + case SHA1: + return "SHA-1"; + case SHA256: + return "SHA-256"; + case SHA512: + return "SHA-512"; + } + throw new UnsupportedOperationException(); + } - public String getHmac() { - switch (this) { - case SHA1: - return "HmacSHA1"; - case SHA256: - return "HmacSHA256"; - case SHA512: - return "HmacSHA512"; - } - throw new UnsupportedOperationException(); - } + public String getHmac() { + switch (this) { + case SHA1: + return "HmacSHA1"; + 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 index 13c92d381e7..e6ea177f4c2 100644 --- 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 @@ -14,7 +14,6 @@ * limitations under the License. */ - package org.apache.activemq.artemis.spi.core.security.scram; import java.security.GeneralSecurityException; @@ -23,24 +22,23 @@ * 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 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); - } + /** + * 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 index 0eda701a488..8a98a05a20f 100644 --- 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 @@ -14,10 +14,8 @@ * 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; @@ -27,279 +25,240 @@ 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 - * @throws InvalidKeyException if internal error occur while working with - * SecretKeySpec - */ - 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 - *

- * 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; - } - } + 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 + *

+ * 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/UserData.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/UserData.java index f56c6a06775..4f6104df8d1 100644 --- 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 @@ -14,43 +14,40 @@ * 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; - + /** + * 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; - } + /** + * 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; + } } From ea5c7a7148fcfe5023a1a959b071e88aca88cd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 08:09:22 +0100 Subject: [PATCH 04/23] ARTEMIS-3106 - Support for SASL-SCRAM add an example server/client --- examples/protocols/amqp/pom.xml | 1 + examples/protocols/amqp/sasl-scram/pom.xml | 51 ++++++++++++++++++ examples/protocols/amqp/sasl-scram/readme.md | 3 ++ .../artemis/jms/example/QPIDClient.java | 53 +++++++++++++++++++ .../artemis/jms/example/TestServer.java | 32 +++++++++++ .../main/resources/artemis-roles.properties | 18 +++++++ .../main/resources/artemis-users.properties | 18 +++++++ .../sasl-scram/src/main/resources/broker.xml | 44 +++++++++++++++ .../sasl-scram/src/main/resources/login.conf | 23 ++++++++ 9 files changed, 243 insertions(+) create mode 100644 examples/protocols/amqp/sasl-scram/pom.xml create mode 100644 examples/protocols/amqp/sasl-scram/readme.md create mode 100644 examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java create mode 100644 examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java create mode 100644 examples/protocols/amqp/sasl-scram/src/main/resources/artemis-roles.properties create mode 100644 examples/protocols/amqp/sasl-scram/src/main/resources/artemis-users.properties create mode 100644 examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml create mode 100644 examples/protocols/amqp/sasl-scram/src/main/resources/login.conf diff --git a/examples/protocols/amqp/pom.xml b/examples/protocols/amqp/pom.xml index 83d4ca556b0..08500679244 100644 --- a/examples/protocols/amqp/pom.xml +++ b/examples/protocols/amqp/pom.xml @@ -51,6 +51,7 @@ under the License. proton-clustered-cpp queue proton-ruby + sasl-scram diff --git a/examples/protocols/amqp/sasl-scram/pom.xml b/examples/protocols/amqp/sasl-scram/pom.xml new file mode 100644 index 00000000000..e83d89f5be2 --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/pom.xml @@ -0,0 +1,51 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.amqp + amqp + 2.18.0-SNAPSHOT + + + sasl-scram + jar + ActiveMQ Artemis SASL-SCRAM Example + + + ${project.basedir}/../../../.. + ${project.version} + + + + + org.apache.qpid + qpid-jms-client + ${qpid.jms.version} + + + org.apache.activemq + artemis-server + ${artemis-version} + + + org.apache.activemq + artemis-amqp-protocol + ${artemis-version} + + + + \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/readme.md b/examples/protocols/amqp/sasl-scram/readme.md new file mode 100644 index 00000000000..900aa8a18e3 --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/readme.md @@ -0,0 +1,3 @@ +# Artemis SASL-SCRAM Server and Client Example + +demonstrate the usage of SASL-SCRAM authentication with Artemis-MQ \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java b/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java new file mode 100644 index 00000000000..3d54620e6df --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java @@ -0,0 +1,53 @@ +/* + *

+ * 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 + *

+ * 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.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 { + for (String method : new String[] { "SCRAM-SHA-1", "SCRAM-SHA-256" }) { + ConnectionFactory connectionFactory = new JmsConnectionFactory( + "amqp://localhost:5672?amqp.saslMechanisms=" + method); + Connection connection; + if ("SCRAM-SHA-256".equals(method)) { + connection = connectionFactory.createConnection("test", "test"); + } else { + connection = connectionFactory.createConnection("hello", "ogre1234"); + } + try { + 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()); + } finally { + connection.close(); + } + } + } +} diff --git a/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java b/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java new file mode 100644 index 00000000000..b069bb5ef17 --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java @@ -0,0 +1,32 @@ +/* + *

+ * 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 + *

+ * 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.jms.example; + +import java.io.File; + +import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; + +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.setConfigResourcePath(new File(configFolder, "broker.xml").getAbsoluteFile().toURI().toASCIIString()); + embedded.start(); + while (true) + ; + + } +} diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-roles.properties b/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-roles.properties new file mode 100644 index 00000000000..083c1e8d940 --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/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/src/main/resources/artemis-users.properties b/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-users.properties new file mode 100644 index 00000000000..a561e9b844e --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-users.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. +## --------------------------------------------------------------------------- +test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +hello = ogre1234 \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml b/examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml new file mode 100644 index 00000000000..a8be415cd9b --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml @@ -0,0 +1,44 @@ + + + + + false + + false + + + tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/login.conf b/examples/protocols/amqp/sasl-scram/src/main/resources/login.conf new file mode 100644 index 00000000000..2a5cb574c3d --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/src/main/resources/login.conf @@ -0,0 +1,23 @@ +/* + * 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"; +}; \ No newline at end of file From 96478f586908c4fcf27dca3912da2a84c0c5fbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 10:59:26 +0100 Subject: [PATCH 05/23] ARTEMIS-3106 - Support for SASL-SCRAM enable Authentication/Login Module --- .../sasl/scram/SCRAMServerSASLFactory.java | 20 ++- .../sasl/scram/ScramServerFunctionality.java | 4 +- .../jaas/AbstractPrincipalLoginModule.java | 120 ++++++++++++++++++ .../security/jaas/JaasCallbackHandler.java | 23 ++-- .../core/security/jaas/Krb5LoginModule.java | 94 +------------- ...5Callback.java => PrincipalsCallback.java} | 23 ++-- .../core/security/jaas/SCRAMLoginModule.java | 25 ++++ .../security/jaas/Krb5LoginModuleTest.java | 13 +- examples/protocols/amqp/sasl-scram/pom.xml | 28 +--- .../amqp/sasl-scram/sasl-client/pom.xml | 41 ++++++ .../sasl-scram/{ => sasl-client}/readme.md | 0 .../artemis/jms/example/QPIDClient.java | 0 .../amqp/sasl-scram/sasl-server/pom.xml | 46 +++++++ .../amqp/sasl-scram/sasl-server/readme.md | 3 + .../artemis/jms/example/TestServer.java | 2 + .../main/resources/artemis-roles.properties | 0 .../main/resources/artemis-users.properties | 0 .../src/main/resources/broker.xml | 2 +- .../src/main/resources/login.conf | 6 + 19 files changed, 297 insertions(+), 153 deletions(-) create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java rename artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/{Krb5Callback.java => PrincipalsCallback.java} (74%) create mode 100644 artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java create mode 100644 examples/protocols/amqp/sasl-scram/sasl-client/pom.xml rename examples/protocols/amqp/sasl-scram/{ => sasl-client}/readme.md (100%) rename examples/protocols/amqp/sasl-scram/{ => sasl-client}/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java (100%) create mode 100644 examples/protocols/amqp/sasl-scram/sasl-server/pom.xml create mode 100644 examples/protocols/amqp/sasl-scram/sasl-server/readme.md rename examples/protocols/amqp/sasl-scram/{ => sasl-server}/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java (88%) rename examples/protocols/amqp/sasl-scram/{ => sasl-server}/src/main/resources/artemis-roles.properties (100%) rename examples/protocols/amqp/sasl-scram/{ => sasl-server}/src/main/resources/artemis-users.properties (100%) rename examples/protocols/amqp/sasl-scram/{ => sasl-server}/src/main/resources/broker.xml (97%) rename examples/protocols/amqp/sasl-scram/{ => sasl-server}/src/main/resources/login.conf (91%) 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 index b61821569ec..bf7eebefe4b 100644 --- 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 @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; +import java.security.Principal; import java.util.Iterator; import java.util.UUID; @@ -45,6 +46,7 @@ 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; +import org.jboss.logging.Logger; /** * abstract class that implements the SASL-SCRAM authentication scheme, concrete implementations @@ -52,6 +54,10 @@ */ public abstract class SCRAMServerSASLFactory implements ServerSASLFactory { + /** + * + */ + private final Logger logger = Logger.getLogger(getClass()); private final SCRAM scramType; public SCRAMServerSASLFactory(SCRAM scram) { @@ -77,7 +83,7 @@ public ServerSASL create(ActiveMQServer server, ProtocolManager new ScramServerFunctionalityImpl(scramType.getDigest(), scramType.getHmac(), UUID.randomUUID().toString()); String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope(); - return new SCRAMServerSASL(scramType.getName(), scram, loginConfigScope); + return new SCRAMServerSASL(scramType.getName(), scram, loginConfigScope, logger); } } catch (NoSuchAlgorithmException e) { // can't be used then... @@ -91,11 +97,13 @@ private static final class SCRAMServerSASL implements ServerSASL { private final ScramServerFunctionality scram; private SASLResult result; private final String loginConfigScope; + private final Logger logger; - SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope) { + SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope, Logger logger) { this.name = name; this.scram = scram; this.loginConfigScope = loginConfigScope; + this.logger = logger; } @Override @@ -135,6 +143,8 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback loginContext.login(); Subject subject = loginContext.getSubject(); Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); + Principal[] principals = subject.getPrincipals().toArray(Principal[]::new); + subject.getPrivateCredentials().add(principals); if (credentials.hasNext()) { result = new SCRAMSASLResult(userName, scram, subject); String challenge = scram.prepareFirstMessage(credentials.next()); @@ -145,10 +155,7 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback } case PREPARED_FIRST: { String finalMessage = scram.prepareFinalMessage(message); - if (finalMessage != null) { - return finalMessage.getBytes(StandardCharsets.US_ASCII); - } - break; + return finalMessage.getBytes(StandardCharsets.US_ASCII); } default: @@ -156,6 +163,7 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback break; } } catch (GeneralSecurityException | ScramException | RuntimeException e) { + logger.warn("SASL-SCRAM Authentication failed", e); result = new SCRAMFailedSASLResult(); } return null; 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 index 12260409ebf..b78575c45b4 100644 --- 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 @@ -16,7 +16,6 @@ package org.apache.activemq.artemis.protocol.amqp.sasl.scram; -import java.security.GeneralSecurityException; import java.security.MessageDigest; import javax.crypto.Mac; @@ -47,10 +46,9 @@ public interface ScramServerFunctionality { * Prepares server's final message * @param clientFinalMessage Client's final message * @return Server's final message - * @throws GeneralSecurityException if there is an error processing clients message * @throws ScramException */ - String prepareFinalMessage(String clientFinalMessage) throws GeneralSecurityException, ScramException; + String prepareFinalMessage(String clientFinalMessage) throws ScramException; /** * Checks if authentication is completed, either successfully or not. Authentication is completed diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java new file mode 100644 index 00000000000..d9c7447d667 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/AbstractPrincipalLoginModule.java @@ -0,0 +1,120 @@ +/* + * 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.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 org.jboss.logging.Logger; + +/** + * Abstract login module that uses an external authenticated principal + */ +public abstract class AbstractPrincipalLoginModule implements AuditLoginModule { + private final Logger logger = Logger.getLogger(getClass()); + + private Subject subject; + private final List authenticatedPrincipals = new LinkedList<>(); + private CallbackHandler callbackHandler; + private boolean loginSucceeded; + private Principal[] principals; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; + this.callbackHandler = callbackHandler; + } + + @Override + public boolean login() throws LoginException { + Callback[] callbacks = new Callback[1]; + + callbacks[0] = new PrincipalsCallback(); + try { + callbackHandler.handle(callbacks); + 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 (!authenticatedPrincipals.isEmpty()) { + loginSucceeded = true; + } + logger.debug("login " + authenticatedPrincipals); + return loginSucceeded; + } + + @Override + public boolean commit() throws LoginException { + boolean result = loginSucceeded; + if (result) { + authenticatedPrincipals.add(new UserPrincipal(authenticatedPrincipals.get(0).getName())); + subject.getPrincipals().addAll(authenticatedPrincipals); + } + + clear(); + + logger.debug("commit, result: " + result); + + return result; + } + + @Override + public boolean abort() throws LoginException { + if (principals != null) { + for (Principal principal : authenticatedPrincipals) { + registerFailureForAudit(principal.getName()); + } + } + clear(); + + logger.debug("abort"); + + return true; + } + + private void clear() { + principals = null; + loginSucceeded = false; + } + + @Override + public boolean logout() throws LoginException { + subject.getPrincipals().removeAll(authenticatedPrincipals); + authenticatedPrincipals.clear(); + clear(); + + logger.debug("logout"); + + return true; + } +} 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 a765f45666c..f994bf1047c 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,11 @@ */ 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 javax.security.auth.Subject; import javax.security.auth.callback.Callback; @@ -25,11 +29,8 @@ 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 +68,22 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback CertificateCallback certCallback = (CertificateCallback) callback; certCallback.setCertificates(getCertsFromConnection(remotingConnection)); - } else if (callback instanceof Krb5Callback) { - Krb5Callback krb5Callback = (Krb5Callback) callback; + } else if (callback instanceof PrincipalsCallback) { + PrincipalsCallback krb5Callback = (PrincipalsCallback) callback; Subject peerSubject = remotingConnection.getSubject(); if (peerSubject != null) { for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) { - krb5Callback.setPeerPrincipal(principal); + krb5Callback.setPeerPrincipals(new Principal[] {principal}); + return; + } + for (Principal[] principals : peerSubject.getPrivateCredentials(Principal[].class)) { + krb5Callback.setPeerPrincipals(principals); return; } } - krb5Callback.setPeerPrincipal(getPeerPrincipalFromConnection(remotingConnection)); + krb5Callback.setPeerPrincipals(new Principal[] {getPeerPrincipalFromConnection(remotingConnection)}); } 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 1047e0827f0..a3cecb1861a 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 principals = new LinkedList<>(); - private CallbackHandler callbackHandler; - private boolean loginSucceeded; - private Principal principal; - - @Override - public void initialize(Subject subject, - CallbackHandler callbackHandler, - Map sharedState, - Map 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% 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/PrincipalsCallback.java index 9306d5f4ac5..69e07d51cbe 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 principal. */ -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/SCRAMLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java new file mode 100644 index 00000000000..4da5200b4a3 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMLoginModule.java @@ -0,0 +1,25 @@ +/* + * 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; + +/** + * Handles the actual login after channel authentication has succeed + */ +public class SCRAMLoginModule extends AbstractPrincipalLoginModule { + + +} 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 d8039a9645b..7a7a2e207d2 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 void loginSuccess() throws Exception { 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/sasl-scram/pom.xml b/examples/protocols/amqp/sasl-scram/pom.xml index e83d89f5be2..2bbc9c222da 100644 --- a/examples/protocols/amqp/sasl-scram/pom.xml +++ b/examples/protocols/amqp/sasl-scram/pom.xml @@ -22,30 +22,12 @@ sasl-scram - jar + pom ActiveMQ Artemis SASL-SCRAM Example - - ${project.basedir}/../../../.. - ${project.version} - - - - - org.apache.qpid - qpid-jms-client - ${qpid.jms.version} - - - org.apache.activemq - artemis-server - ${artemis-version} - - - org.apache.activemq - artemis-amqp-protocol - ${artemis-version} - - + + sasl-client + sasl-server + \ 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 00000000000..9d795dc3231 --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml @@ -0,0 +1,41 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.amqp + sasl-scram + 2.18.0-SNAPSHOT + + + sasl-scram-client + jar + ActiveMQ Artemis SASL-SCRAM-Client Example + + + ${project.basedir}/../../../.. + ${project.version} + + + + + org.apache.qpid + qpid-jms-client + ${qpid.jms.version} + + + + \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/readme.md b/examples/protocols/amqp/sasl-scram/sasl-client/readme.md similarity index 100% rename from examples/protocols/amqp/sasl-scram/readme.md rename to examples/protocols/amqp/sasl-scram/sasl-client/readme.md diff --git a/examples/protocols/amqp/sasl-scram/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 similarity index 100% rename from examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java rename to examples/protocols/amqp/sasl-scram/sasl-client/src/main/java/org/apache/activemq/artemis/jms/example/QPIDClient.java 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 00000000000..01008ec7bcc --- /dev/null +++ b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml @@ -0,0 +1,46 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.amqp + sasl-scram + 2.18.0-SNAPSHOT + + + sasl-scram-server + jar + ActiveMQ Artemis SASL-SCRAM-Server Example + + + ${project.basedir}/../../../.. + ${project.version} + + + + + org.apache.activemq + artemis-server + ${artemis-version} + + + org.apache.activemq + artemis-amqp-protocol + ${artemis-version} + + + + \ 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 00000000000..900aa8a18e3 --- /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 Artemis-MQ \ No newline at end of file diff --git a/examples/protocols/amqp/sasl-scram/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 similarity index 88% rename from examples/protocols/amqp/sasl-scram/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java rename to examples/protocols/amqp/sasl-scram/sasl-server/src/main/java/org/apache/activemq/artemis/jms/example/TestServer.java index b069bb5ef17..7677d24a564 100644 --- a/examples/protocols/amqp/sasl-scram/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 @@ -17,12 +17,14 @@ 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) diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-roles.properties b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-roles.properties similarity index 100% rename from examples/protocols/amqp/sasl-scram/src/main/resources/artemis-roles.properties rename to examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-roles.properties diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/artemis-users.properties b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-users.properties similarity index 100% rename from examples/protocols/amqp/sasl-scram/src/main/resources/artemis-users.properties rename to examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/artemis-users.properties diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml similarity index 97% rename from examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml rename to examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml index a8be415cd9b..6e17546f5a8 100644 --- a/examples/protocols/amqp/sasl-scram/src/main/resources/broker.xml +++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml @@ -24,7 +24,7 @@ under the License. false - false + true tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram diff --git a/examples/protocols/amqp/sasl-scram/src/main/resources/login.conf b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf similarity index 91% rename from examples/protocols/amqp/sasl-scram/src/main/resources/login.conf rename to examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf index 2a5cb574c3d..7fad76fd3a3 100644 --- a/examples/protocols/amqp/sasl-scram/src/main/resources/login.conf +++ b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/login.conf @@ -20,4 +20,10 @@ amqp-sasl-scram { 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 From 7f6f612bc688cd0e1e359476e82c329ea5819f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Feb 2021 16:01:24 +0100 Subject: [PATCH 06/23] java8 compatibility --- .../protocol/amqp/sasl/scram/SCRAMServerSASLFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index bf7eebefe4b..9692d8f780a 100644 --- 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 @@ -143,7 +143,7 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback loginContext.login(); Subject subject = loginContext.getSubject(); Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); - Principal[] principals = subject.getPrincipals().toArray(Principal[]::new); + Principal[] principals = subject.getPrincipals().toArray(new Principal[0]); subject.getPrivateCredentials().add(principals); if (credentials.hasNext()) { result = new SCRAMSASLResult(userName, scram, subject); From fb48a356fc0ff3436029f99807203c9c86677c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 2 Mar 2021 07:48:38 +0100 Subject: [PATCH 07/23] ARTEMIS-3106 - Support for SASL-SCRAM Add integration test, add info to the notice file --- NOTICE | 5 ++ .../amqp/connect/SaslScramTest.java | 87 +++++++++++++++++++ .../resources/artemis-scram-roles.properties | 18 ++++ .../resources/artemis-scram-users.properties | 18 ++++ .../src/test/resources/broker-saslscram.xml | 44 ++++++++++ .../src/test/resources/login.config | 12 +++ 6 files changed, 184 insertions(+) create mode 100644 tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java create mode 100644 tests/integration-tests/src/test/resources/artemis-scram-roles.properties create mode 100644 tests/integration-tests/src/test/resources/artemis-scram-users.properties create mode 100644 tests/integration-tests/src/test/resources/broker-saslscram.xml diff --git a/NOTICE b/NOTICE index c9d626cc7d6..4024800f518 100644 --- a/NOTICE +++ b/NOTICE @@ -3,3 +3,8 @@ Copyright [2014-2021] The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). + +The Initial Developer of the ScramServerFunctionality.java, ScramServerFunctionalityImpl.java, +ScramException.java, ScramUtils.java, UserData.java +is "SCRAM SASL authentication for Java" (https://github.com/ogrebgr/scram-sasl) +Copyright 2016 Ognyan Bankov, Licensed under the Apache License, Version 2.0 diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java new file mode 100644 index 00000000000..d7a2d3b504b --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java @@ -0,0 +1,87 @@ +/** + * 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.tests.integration.amqp.connect; + +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.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.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(); + } + + @Test + public void testSendReceive() throws JMSException { + for (String method : new String[] {"SCRAM-SHA-1", "SCRAM-SHA-256"}) { + ConnectionFactory connectionFactory = + new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method); + Connection connection; + if ("SCRAM-SHA-256".equals(method)) { + connection = connectionFactory.createConnection("test", "test"); + } else { + connection = connectionFactory.createConnection("hello", "ogre1234"); + } + try { + 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()); + } finally { + connection.close(); + } + } + } +} 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 00000000000..083c1e8d940 --- /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 +admin=test \ 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 00000000000..a561e9b844e --- /dev/null +++ b/tests/integration-tests/src/test/resources/artemis-scram-users.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. +## --------------------------------------------------------------------------- +test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +hello = ogre1234 \ No newline at end of file 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 00000000000..6e17546f5a8 --- /dev/null +++ b/tests/integration-tests/src/test/resources/broker-saslscram.xml @@ -0,0 +1,44 @@ + + + + + false + + true + + + tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram + + + + + + + + + + + + + + \ 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 a70836b1267..6ff980c1830 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 + ; +}; From 02d1c111804742c09b3e31a81acf9c62e973d39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 10 Mar 2021 08:17:27 +0100 Subject: [PATCH 08/23] ARTEMIS-3106 - Support for SASL-SCRAM Adjust to review comments --- NOTICE | 5 -- .../sasl/scram/SCRAMServerSASLFactory.java | 3 - .../scram/SHA512CRAMServerSASLFactory.java | 34 ------- ...temis.protocol.amqp.sasl.ServerSASLFactory | 3 +- .../security/jaas/JaasCallbackHandler.java | 8 +- .../security/jaas/PrincipalsCallback.java | 2 +- examples/protocols/amqp/pom.xml | 2 +- examples/protocols/amqp/sasl-scram/pom.xml | 59 ++++++------ .../amqp/sasl-scram/sasl-client/pom.xml | 80 +++++++++-------- .../amqp/sasl-scram/sasl-client/readme.md | 2 +- .../artemis/jms/example/QPIDClient.java | 37 ++++---- .../amqp/sasl-scram/sasl-server/pom.xml | 90 ++++++++++--------- .../amqp/sasl-scram/sasl-server/readme.md | 2 +- .../sasl-server/src/main/resources/broker.xml | 48 +++++----- tests/integration-tests/.gitignore | 2 + .../amqp/{connect => sasl}/SaslScramTest.java | 55 +++++++----- .../src/test/resources/broker-saslscram.xml | 48 +++++----- 17 files changed, 238 insertions(+), 242 deletions(-) delete mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java create mode 100644 tests/integration-tests/.gitignore rename tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/{connect => sasl}/SaslScramTest.java (59%) diff --git a/NOTICE b/NOTICE index 4024800f518..c9d626cc7d6 100644 --- a/NOTICE +++ b/NOTICE @@ -3,8 +3,3 @@ Copyright [2014-2021] The Apache Software Foundation This product includes software developed at The Apache Software Foundation (http://www.apache.org/). - -The Initial Developer of the ScramServerFunctionality.java, ScramServerFunctionalityImpl.java, -ScramException.java, ScramUtils.java, UserData.java -is "SCRAM SASL authentication for Java" (https://github.com/ogrebgr/scram-sasl) -Copyright 2016 Ognyan Bankov, Licensed under the Apache License, Version 2.0 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 index 9692d8f780a..efe5ed20607 100644 --- 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 @@ -54,9 +54,6 @@ */ public abstract class SCRAMServerSASLFactory implements ServerSASLFactory { - /** - * - */ private final Logger logger = Logger.getLogger(getClass()); private final SCRAM scramType; diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java deleted file mode 100644 index 21bf98e43ca..00000000000 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512CRAMServerSASLFactory.java +++ /dev/null @@ -1,34 +0,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. - */ -package org.apache.activemq.artemis.protocol.amqp.sasl.scram; - -import org.apache.activemq.artemis.spi.core.security.scram.SCRAM; - -/** - * provides SASL SCRAM-SHA512 - */ -public class SHA512CRAMServerSASLFactory extends SCRAMServerSASLFactory { - - public SHA512CRAMServerSASLFactory() { - super(SCRAM.SHA512); - } - - @Override - public int getPrecedence() { - return 500; - } -} 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 10f411622a2..366f9111f50 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 @@ -3,5 +3,4 @@ org.apache.activemq.artemis.protocol.amqp.sasl.PlainServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA1SCRAMServerSASLFactory -org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory -org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512CRAMServerSASLFactory \ No newline at end of file +org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory \ No newline at end of file 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 f994bf1047c..0534243c054 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 @@ -69,21 +69,21 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback certCallback.setCertificates(getCertsFromConnection(remotingConnection)); } else if (callback instanceof PrincipalsCallback) { - PrincipalsCallback krb5Callback = (PrincipalsCallback) callback; + PrincipalsCallback principalsCallback = (PrincipalsCallback) callback; Subject peerSubject = remotingConnection.getSubject(); if (peerSubject != null) { for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) { - krb5Callback.setPeerPrincipals(new Principal[] {principal}); + principalsCallback.setPeerPrincipals(new Principal[] {principal}); return; } for (Principal[] principals : peerSubject.getPrivateCredentials(Principal[].class)) { - krb5Callback.setPeerPrincipals(principals); + principalsCallback.setPeerPrincipals(principals); return; } } - krb5Callback.setPeerPrincipals(new Principal[] {getPeerPrincipalFromConnection(remotingConnection)}); + principalsCallback.setPeerPrincipals(new Principal[] {getPeerPrincipalFromConnection(remotingConnection)}); } else { throw new UnsupportedCallbackException(callback); } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java index 69e07d51cbe..821f6fb9e05 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/PrincipalsCallback.java @@ -21,7 +21,7 @@ import javax.security.auth.callback.Callback; /** - * A Callback for getting the peer principal. + * A Callback for getting the peer principals. */ public class PrincipalsCallback implements Callback { diff --git a/examples/protocols/amqp/pom.xml b/examples/protocols/amqp/pom.xml index 08500679244..b9b1bbeab49 100644 --- a/examples/protocols/amqp/pom.xml +++ b/examples/protocols/amqp/pom.xml @@ -51,7 +51,7 @@ under the License. proton-clustered-cpp queue proton-ruby - sasl-scram + sasl-scram diff --git a/examples/protocols/amqp/sasl-scram/pom.xml b/examples/protocols/amqp/sasl-scram/pom.xml index 2bbc9c222da..b8c0c717f7c 100644 --- a/examples/protocols/amqp/sasl-scram/pom.xml +++ b/examples/protocols/amqp/sasl-scram/pom.xml @@ -1,33 +1,40 @@ - + - - org.apache.activemq.examples.amqp - amqp - 2.18.0-SNAPSHOT - + + 4.0.0 - sasl-scram - pom - ActiveMQ Artemis SASL-SCRAM Example + + org.apache.activemq.examples.amqp + amqp + 2.18.0-SNAPSHOT + - - sasl-client - sasl-server - + sasl-scram + pom + ActiveMQ Artemis SASL-SCRAM Example + + sasl-client + sasl-server + \ 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 index 9d795dc3231..56549883ef6 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml +++ b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml @@ -1,41 +1,49 @@ - + - 4.0.0 - - - org.apache.activemq.examples.amqp - sasl-scram - 2.18.0-SNAPSHOT - - - sasl-scram-client - jar - ActiveMQ Artemis SASL-SCRAM-Client Example - - - ${project.basedir}/../../../.. - ${project.version} - - - - - org.apache.qpid - qpid-jms-client - ${qpid.jms.version} - - + 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"> + 4.0.0 + + + org.apache.activemq.examples.amqp + sasl-scram + 2.18.0-SNAPSHOT + + + sasl-scram-client + jar + ActiveMQ Artemis SASL-SCRAM-Client Example + + + ${project.basedir}/../../../.. + ${project.version} + + + + + org.apache.qpid + qpid-jms-client + ${qpid.jms.version} + + \ 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 index 900aa8a18e3..50fe5d7ca7a 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-client/readme.md +++ b/examples/protocols/amqp/sasl-scram/sasl-client/readme.md @@ -1,3 +1,3 @@ # Artemis SASL-SCRAM Server and Client Example -demonstrate the usage of SASL-SCRAM authentication with Artemis-MQ \ No newline at end of file +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 index 3d54620e6df..d5d4cbd3517 100644 --- 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 @@ -27,27 +27,22 @@ public class QPIDClient { public static void main(String[] args) throws JMSException { - for (String method : new String[] { "SCRAM-SHA-1", "SCRAM-SHA-256" }) { - ConnectionFactory connectionFactory = new JmsConnectionFactory( - "amqp://localhost:5672?amqp.saslMechanisms=" + method); - Connection connection; - if ("SCRAM-SHA-256".equals(method)) { - connection = connectionFactory.createConnection("test", "test"); - } else { - connection = connectionFactory.createConnection("hello", "ogre1234"); - } - try { - 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()); - } finally { - connection.close(); - } + 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 index 01008ec7bcc..62d6d05a186 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml +++ b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml @@ -1,46 +1,54 @@ - + - 4.0.0 - - - org.apache.activemq.examples.amqp - sasl-scram - 2.18.0-SNAPSHOT - - - sasl-scram-server - jar - ActiveMQ Artemis SASL-SCRAM-Server Example - - - ${project.basedir}/../../../.. - ${project.version} - - - - - org.apache.activemq - artemis-server - ${artemis-version} - - - org.apache.activemq - artemis-amqp-protocol - ${artemis-version} - - + 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"> + 4.0.0 + + + org.apache.activemq.examples.amqp + sasl-scram + 2.18.0-SNAPSHOT + + + sasl-scram-server + jar + ActiveMQ Artemis SASL-SCRAM-Server Example + + + ${project.basedir}/../../../.. + ${project.version} + + + + + org.apache.activemq + artemis-server + ${artemis-version} + + + org.apache.activemq + artemis-amqp-protocol + ${artemis-version} + + \ 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 index 900aa8a18e3..1cc11ed2f70 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-server/readme.md +++ b/examples/protocols/amqp/sasl-scram/sasl-server/readme.md @@ -1,3 +1,3 @@ # Artemis SASL-SCRAM Server and Client Example -demonstrate the usage of SASL-SCRAM authentication with Artemis-MQ \ No newline at end of file +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/resources/broker.xml b/examples/protocols/amqp/sasl-scram/sasl-server/src/main/resources/broker.xml index 6e17546f5a8..a0252ac7e31 100644 --- 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 @@ -1,3 +1,4 @@ + - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="urn:activemq" + xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd"> + - false + false - true + true - - tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram - - - - - - - - - - - - - + + tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/integration-tests/.gitignore b/tests/integration-tests/.gitignore new file mode 100644 index 00000000000..00d2ab71ddb --- /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/SaslScramTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java similarity index 59% rename from tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java rename to tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java index d7a2d3b504b..bfb60436343 100644 --- a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/SaslScramTest.java +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/sasl/SaslScramTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.artemis.tests.integration.amqp.connect; +package org.apache.activemq.artemis.tests.integration.amqp.sasl; import static org.junit.Assert.assertEquals; @@ -32,6 +32,7 @@ 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; @@ -46,6 +47,7 @@ public class SaslScramTest { @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()); @@ -59,29 +61,34 @@ public static void shutdownBroker() throws Exception { } @Test - public void testSendReceive() throws JMSException { - for (String method : new String[] {"SCRAM-SHA-1", "SCRAM-SHA-256"}) { - ConnectionFactory connectionFactory = - new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method); - Connection connection; - if ("SCRAM-SHA-256".equals(method)) { - connection = connectionFactory.createConnection("test", "test"); - } else { - connection = connectionFactory.createConnection("hello", "ogre1234"); - } - try { - 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()); - } finally { - connection.close(); - } + public void testUnencryptedWorksWithAllMechanism() throws JMSException { + sendRcv("SCRAM-SHA-1", "hello", "ogre1234"); + sendRcv("SCRAM-SHA-256", "hello", "ogre1234"); + } + + @Test(expected = JMSSecuritySaslException.class) + public void testEncryptedWorksOnlyWithMechanism() throws JMSException { + sendRcv("SCRAM-SHA-1", "test", "test"); + } + + @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/broker-saslscram.xml b/tests/integration-tests/src/test/resources/broker-saslscram.xml index 6e17546f5a8..a0252ac7e31 100644 --- a/tests/integration-tests/src/test/resources/broker-saslscram.xml +++ b/tests/integration-tests/src/test/resources/broker-saslscram.xml @@ -1,3 +1,4 @@ + - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns="urn:activemq" + xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd"> + - false + false - true + true - - tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram - - - - - - - - - - - - - + + tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram + + + + + + + + + + + + + + \ No newline at end of file From 08ae9868effe2a5a49103ba7460ffb366fb4d1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 10 Mar 2021 09:52:07 +0100 Subject: [PATCH 09/23] Fix path --- examples/protocols/amqp/sasl-scram/sasl-client/pom.xml | 2 +- examples/protocols/amqp/sasl-scram/sasl-server/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml index 56549883ef6..c05fde8f51a 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml +++ b/examples/protocols/amqp/sasl-scram/sasl-client/pom.xml @@ -34,7 +34,7 @@ under the License. ActiveMQ Artemis SASL-SCRAM-Client Example - ${project.basedir}/../../../.. + ${project.basedir}/../../../../.. ${project.version} diff --git a/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml index 62d6d05a186..e1683ab662a 100644 --- a/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml +++ b/examples/protocols/amqp/sasl-scram/sasl-server/pom.xml @@ -34,7 +34,7 @@ under the License. ActiveMQ Artemis SASL-SCRAM-Server Example - ${project.basedir}/../../../.. + ${project.basedir}/../../../../.. ${project.version} From ace346566844dca2c41c8e814ed94ec8680853f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 18 Mar 2021 17:08:24 +0100 Subject: [PATCH 10/23] ARTEMIS-3106 - Support for SASL-SCRAM Adjust to review comments --- .../jaas/SCRAMPropertiesLoginModule.java | 43 +++++++++++-------- .../spi/core/security/scram/SCRAM.java | 9 +--- 2 files changed, 26 insertions(+), 26 deletions(-) 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 index 6c406400418..5507546bbf2 100644 --- 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 @@ -51,6 +51,7 @@ public class SCRAMPropertiesLoginModule extends PropertiesLoader implements Audi private static final String SEPARATOR = ":"; private static final int MIN_ITERATIONS = 4096; + private static final SecureRandom RANDOM_GENERATOR = new SecureRandom(); private Subject subject; private CallbackHandler callbackHandler; private Properties users; @@ -81,33 +82,39 @@ public boolean login() throws LoginException { } String password = users.getProperty(user); if (password == null) { - throw new FailedLoginException("User does not exist: " + user); - } - if (PasswordMaskingUtil.isEncMasked(password)) { + // if the user is not available 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); + userData = generateUserData(new String(randomPassword)); + } else if (PasswordMaskingUtil.isEncMasked(password)) { String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR); userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]); } else { - DigestCallback digestCallback = new DigestCallback(); - HmacCallback hmacCallback = new HmacCallback(); - executeCallbacks(new Callback[] {digestCallback, hmacCallback}); - byte[] salt = generateSalt(); - try { - ScramUtils.NewPasswordStringData data = - ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, 4096, - digestCallback.getDigest(), - hmacCallback.getHmac())); - userData = new UserData(data.salt, data.iterations, data.serverKey, data.storedKey); - } catch (ScramException e) { - throw new LoginException(); - } + userData = generateUserData(password); } return true; } + private UserData generateUserData(String plainTextPassword) throws LoginException { + DigestCallback digestCallback = new DigestCallback(); + HmacCallback hmacCallback = new HmacCallback(); + executeCallbacks(new Callback[] {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[24]; - SecureRandom random = new SecureRandom(); - random.nextBytes(salt); + RANDOM_GENERATOR.nextBytes(salt); return salt; } 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 index f994a0d2a09..bac21f433bb 100644 --- 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 @@ -21,8 +21,7 @@ */ public enum SCRAM { SHA1, - SHA256, - SHA512; + SHA256; public String getName() { switch (this) { @@ -30,8 +29,6 @@ public String getName() { return "SCRAM-SHA-1"; case SHA256: return "SCRAM-SHA-256"; - case SHA512: - return "SCRAM-SHA-512"; } throw new UnsupportedOperationException(); } @@ -42,8 +39,6 @@ public String getDigest() { return "SHA-1"; case SHA256: return "SHA-256"; - case SHA512: - return "SHA-512"; } throw new UnsupportedOperationException(); } @@ -54,8 +49,6 @@ public String getHmac() { return "HmacSHA1"; case SHA256: return "HmacSHA256"; - case SHA512: - return "HmacSHA512"; } throw new UnsupportedOperationException(); } From 49148b8af2711a979161335972692d90e52d1720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 18 Mar 2021 17:13:47 +0100 Subject: [PATCH 11/23] ARTEMIS-3106 - Support for SASL-SCRAM add comment about encoded/unencoded --- .../sasl-server/src/main/resources/artemis-users.properties | 4 ++++ .../src/test/resources/artemis-scram-users.properties | 3 +++ 2 files changed, 7 insertions(+) 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 index a561e9b844e..e2f2b4c9939 100644 --- 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 @@ -14,5 +14,9 @@ ## 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 [] test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +# Example for a plain username/password, don't use this on public servers! hello = ogre1234 \ 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 index a561e9b844e..18f89f8f124 100644 --- a/tests/integration-tests/src/test/resources/artemis-scram-users.properties +++ b/tests/integration-tests/src/test/resources/artemis-scram-users.properties @@ -14,5 +14,8 @@ ## 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 [] test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +# Example for a plain username/password, don't use this on public servers! hello = ogre1234 \ No newline at end of file From 997bcdf4ac3d5941dc141d509c01ef58b11548f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 18 Mar 2021 17:17:14 +0100 Subject: [PATCH 12/23] ARTEMIS-3106 - Support for SASL-SCRAM checkstyle --- examples/protocols/amqp/sasl-scram/pom.xml | 3 ++ .../artemis/jms/example/QPIDClient.java | 36 +++++++++---------- .../artemis/jms/example/TestServer.java | 21 +++++------ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/examples/protocols/amqp/sasl-scram/pom.xml b/examples/protocols/amqp/sasl-scram/pom.xml index b8c0c717f7c..a7b34e8d030 100644 --- a/examples/protocols/amqp/sasl-scram/pom.xml +++ b/examples/protocols/amqp/sasl-scram/pom.xml @@ -28,6 +28,9 @@ under the License. amqp 2.18.0-SNAPSHOT + + ${project.basedir}/../../../.. + sasl-scram pom 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 index d5d4cbd3517..5e4dd2ec66f 100644 --- 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 @@ -26,23 +26,23 @@ 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"); - } + 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()); - } - } + 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/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 index 7677d24a564..4bb92d3f1cf 100644 --- 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 @@ -20,15 +20,16 @@ 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) - ; + 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 + } - } + } } From 97f941207716cdebd13603e0da8da4d1f1322368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 23 Mar 2021 07:34:27 +0100 Subject: [PATCH 13/23] ARTEMIS-3106 - Support for SASL-SCRAM Additional storage of alternative encoded credentials --- .../jaas/SCRAMPropertiesLoginModule.java | 83 +++++++++++-------- .../main/resources/artemis-users.properties | 5 +- .../integration/amqp/sasl/SaslScramTest.java | 23 +++++ .../resources/artemis-scram-roles.properties | 4 +- .../resources/artemis-scram-users.properties | 11 ++- 5 files changed, 85 insertions(+), 41 deletions(-) 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 index 5507546bbf2..886e0d43b45 100644 --- 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 @@ -33,7 +33,6 @@ import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.FailedLoginException; import javax.security.auth.login.LoginException; import org.apache.activemq.artemis.spi.core.security.scram.SCRAM; @@ -41,6 +40,7 @@ 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.utils.PasswordMaskingUtil; +import org.jgroups.util.UUID; /** * Login modules that uses properties files similar to the {@link PropertiesLoginModule}. It can @@ -49,7 +49,11 @@ */ public class SCRAMPropertiesLoginModule extends PropertiesLoader implements AuditLoginModule { - private static final String SEPARATOR = ":"; + /** + * + */ + 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; @@ -75,20 +79,17 @@ public void initialize(Subject subject, CallbackHandler callbackHandler, Map []"); + " []"); 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]; - String type = args[2]; + 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)); + System.out.println(username + SEPARATOR_MECHANISM + scram.name() + " = " + + PasswordMaskingUtil.wrap(data.salt + SEPARATOR_PARAMETER + data.iterations + SEPARATOR_PARAMETER + + data.serverKey + SEPARATOR_PARAMETER + data.storedKey)); + } + } + + 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())); - MessageDigest digest = MessageDigest.getInstance(scram.getDigest()); - Mac hmac = Mac.getInstance(scram.getHmac()); - byte[] salt = generateSalt(); - int iterations; - if (args.length > 3) { - iterations = Integer.parseInt(args[3]); - 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)); - System.out.println(username + " = " + PasswordMaskingUtil.wrap(data.salt + SEPARATOR + data.iterations + - SEPARATOR + data.serverKey + SEPARATOR + data.storedKey)); + return scram; } private static String getSupportedTypes() { 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 index e2f2b4c9939..fb524450712 100644 --- 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 @@ -16,7 +16,8 @@ ## --------------------------------------------------------------------------- ## -# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule [] -test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule [] +test|SHA1 = ENC(eI//JkJhVGYF8JX0ce8jCWkRE0BgNVuJ:4096:A/R04d4qd1y76W7FkWV67bhz68A=:p758FRkQekdJyLisYVBZiXMG0ac=) +test|SHA256 = ENC(pzWgKU7rGWuP6ZZpfqZdhXUw5OX+FMAy:4096:ArrBsIj8rLUPZh9yfHagQ5h7MfcxkkfguqyJTGRYKTg=:Ieqpx2kzV+VlZ26cIdTSoClmE8YyM3s8hx57LrZUr+Q=) # Example for a plain username/password, don't use this on public servers! hello = ogre1234 \ No newline at end of file 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 index bfb60436343..76b1d4f909a 100644 --- 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 @@ -60,17 +60,40 @@ 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"); diff --git a/tests/integration-tests/src/test/resources/artemis-scram-roles.properties b/tests/integration-tests/src/test/resources/artemis-scram-roles.properties index 083c1e8d940..ccff1c4e030 100644 --- a/tests/integration-tests/src/test/resources/artemis-scram-roles.properties +++ b/tests/integration-tests/src/test/resources/artemis-scram-roles.properties @@ -14,5 +14,5 @@ ## 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 +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 index 18f89f8f124..babb223936d 100644 --- a/tests/integration-tests/src/test/resources/artemis-scram-users.properties +++ b/tests/integration-tests/src/test/resources/artemis-scram-users.properties @@ -15,7 +15,12 @@ ## 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 [] -test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule [] +multi|SHA1 = ENC(qWyghLZUUGAsJxjDWy6iAvqVzb5br909:4096:bhM3xnt1XrPmPepxH+oy8YS0St0=:SpJQgsIxwYrbwb8Y5/QjuvWhEms=) +multi|SHA256 = ENC(qvzXWU1m8/9oyTKpomFmEfZNVTnCagmj:4096:5GPhT3fg5d5BJbMg0U8XBMXBvdc1YzKAM9j0jmSSd5k=:5X+WXPyhrgMQ3u0tuWDwDPtZdd+t/tFr01ccGkr2TK0=) + # Example for a plain username/password, don't use this on public servers! -hello = ogre1234 \ No newline at end of file +hello = ogre1234 + +# just for unit-test purpose! +test = ENC(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) From edaa7916d3d020f38047993632809a3f92479241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 23 Mar 2021 08:41:56 +0100 Subject: [PATCH 14/23] ARTEMIS-3106 - Support for SASL-SCRAM copy subject and perform logout on failure --- .../sasl/scram/SCRAMServerSASLFactory.java | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) 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 index efe5ed20607..ec0f7ad95d5 100644 --- 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 @@ -21,6 +21,7 @@ import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.util.Collections; import java.util.Iterator; import java.util.UUID; @@ -30,6 +31,7 @@ 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; @@ -111,13 +113,14 @@ public String getName() { @Override public byte[] processSASL(byte[] bytes) { String message = new String(bytes, StandardCharsets.US_ASCII); + LoginContext loginContext = null; try { switch (scram.getState()) { case INITIAL: { String userName = scram.handleClientFirstMessage(message); if (userName != null) { - LoginContext loginContext = new LoginContext(loginConfigScope, new CallbackHandler() { + loginContext = new LoginContext(loginConfigScope, new CallbackHandler() { @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { @@ -138,14 +141,23 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback } }); loginContext.login(); - Subject subject = loginContext.getSubject(); - Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); - Principal[] principals = subject.getPrincipals().toArray(new Principal[0]); - subject.getPrivateCredentials().add(principals); - if (credentials.hasNext()) { - result = new SCRAMSASLResult(userName, scram, subject); - String challenge = scram.prepareFirstMessage(credentials.next()); - return challenge.getBytes(StandardCharsets.US_ASCII); + try { + Subject subject = loginContext.getSubject(); + Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); + Principal[] principals = subject.getPrincipals().toArray(new Principal[0]); + Subject saslSubject = new Subject(true, subject.getPrincipals(), subject.getPublicCredentials(), + Collections.singleton(principals)); + if (credentials.hasNext()) { + result = new SCRAMSASLResult(userName, scram, saslSubject); + String challenge = scram.prepareFirstMessage(credentials.next()); + return challenge.getBytes(StandardCharsets.US_ASCII); + } + } finally { + try { + loginContext.logout(); + } catch (LoginException e1) { + // we can't do anything useful then but also don'T want to fail here... + } } } break; @@ -162,6 +174,13 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback } catch (GeneralSecurityException | ScramException | RuntimeException e) { logger.warn("SASL-SCRAM Authentication failed", e); result = new SCRAMFailedSASLResult(); + if (loginContext != null) { + try { + loginContext.logout(); + } catch (LoginException e1) { + // we can't do anything useful then... + } + } } return null; } From a91948ab2d2d37d97acaf0b9f6842d36db660e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 23 Mar 2021 14:28:56 +0100 Subject: [PATCH 15/23] ARTEMIS-3106 - Support for SASL-SCRAM use 256 bits as salt --- .../spi/core/security/jaas/SCRAMPropertiesLoginModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 886e0d43b45..1a61f0d01a8 100644 --- 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 @@ -122,7 +122,7 @@ private UserData generateUserData(String plainTextPassword) throws LoginExceptio } private static byte[] generateSalt() { - byte[] salt = new byte[24]; + byte[] salt = new byte[32]; RANDOM_GENERATOR.nextBytes(salt); return salt; } From ed6eef9e2e9c9fb56919d923a5a767c26513d468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 24 Mar 2021 09:52:30 +0100 Subject: [PATCH 16/23] ARTEMIS-3106 - Support for SASL-SCRAM add client sasl for broker interconnect --- .../amqp/connect/AMQPBrokerConnection.java | 16 +- .../protocol/amqp/sasl/scram/Normalizer.java | 87 + .../amqp/sasl/scram/SCRAMClientSASL.java | 95 + .../amqp/sasl/scram/SCRAMServerSASL.java | 156 ++ .../sasl/scram/SCRAMServerSASLFactory.java | 197 +- .../scram/SHA1SCRAMServerSASLFactory.java | 2 +- .../scram/SHA256SCRAMServerSASLFactory.java | 2 +- .../scram/SHA512SCRAMServerSASLFactory.java | 34 + .../sasl/scram/ScramClientFunctionality.java | 92 + .../scram/ScramClientFunctionalityImpl.java | 214 ++ .../protocol/amqp/sasl/scram/StringPrep.java | 2138 +++++++++++++++++ ...temis.protocol.amqp.sasl.ServerSASLFactory | 3 +- .../spi/core/security/scram/SCRAM.java | 12 +- .../amqp/connect/AMQPConnectSaslTest.java | 158 +- 14 files changed, 3027 insertions(+), 179 deletions(-) create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASL.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionality.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionalityImpl.java create mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java 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 a95914fce08..2068d116eb0 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.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 senders = new HashSet<>(); - private Set receivers = new HashSet<>(); + private final Set senders = new HashSet<>(); + private final Set receivers = new HashSet<>(); final Executor connectExecutor; final ScheduledExecutorService scheduledExecutorService; @@ -676,7 +678,15 @@ public ClientSASL chooseMechanism(String[] offeredMechanims) { 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/Normalizer.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java new file mode 100644 index 00000000000..9e5f41b12e3 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java @@ -0,0 +1,87 @@ +/** + * Copyright 2011 Glenn Maynard + *

+ * 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 + *

+ * 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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Dynamically-loaded interface for java.text.Normalizer(NFKC); it's missing in non-bleeding-edge + * versions of Android and we can get by without it. + */ +@SuppressWarnings({ "SpellCheckingInspection", "JavaDoc" }) +class Normalizer { + private static boolean initialized = false; + private static Method normalize; // java.text.Normalizer.normalize + private static Object nfkc; // java.text.Normalizer.Form.NFKC + private static final Object lock = new Object(); + + /** + * Equivalent to {@link java.text.Normalizer#normalize}(seq, Normalizer.Form.NFKC). If Normalizer + * is unavailable, returns the sequence unchanged. + */ + public static String normalize(CharSequence seq) { + synchronized (lock) { + if (!initialized) { + initialized = true; + initialize("java.text.Normalizer"); + } + } + + if (normalize == null) + return seq.toString(); + + try { + return (String) normalize.invoke(null, seq, nfkc); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("SameParameterValue") + private static void initialize(String classPath) { + try { + Class normalizerClass = Class.forName(classPath); + Class normalizerFormClass = findSubclassByName(normalizerClass); + Object[] normalizerConstants = normalizerFormClass.getEnumConstants(); + + nfkc = findObjectByValue(normalizerConstants, "NFKC"); + normalize = normalizerClass.getMethod("normalize", CharSequence.class, normalizerFormClass); + } catch (SecurityException | ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalStateException("Couldn't load java.text.Normalizer", e); + } + } + + private static Class findSubclassByName(Class parentClass) throws ClassNotFoundException { + Class[] subClasses = parentClass.getClasses(); + String searchForName = parentClass.getName() + "$Form"; + for (Class memberClass : subClasses) { + String s = memberClass.getName(); + if (s.equals(searchForName)) + return memberClass; + } + throw new ClassNotFoundException(); + } + + @SuppressWarnings("SameParameterValue") + private static Object findObjectByValue(Object[] objects, String s) throws ClassNotFoundException { + for (Object e : objects) { + if (e.toString().equals(s)) + return e; + } + throw new ClassNotFoundException(); + } +} 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 00000000000..5f7ef218a3c --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java @@ -0,0 +1,95 @@ +/* + * 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) { + 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(), UUID.randomUUID().toString()); + } + + @Override + public String getName() { + return scramType.getName(); + } + + @Override + public byte[] getInitialResponse() { + try { + String firstMessage = client.prepareFirstMessage(username); + System.out.println("SCRAMClientSASL.getInitialResponse() >> " + firstMessage); + 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); + System.out.println("SCRAMClientSASL.getResponse() << " + msg); + if (client.getState() == State.FIRST_PREPARED) { + try { + String finalMessage = client.prepareFinalMessage(password, msg); + System.out.println("SCRAMClientSASL.getResponse() >> " + finalMessage); + 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 00000000000..456c77ea851 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SCRAMServerSASL.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.activemq.artemis.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; +import org.jboss.logging.Logger; + +public abstract class SCRAMServerSASL implements ServerSASL { + + protected final ScramServerFunctionality scram; + protected final SCRAM mechanism; + private SASLResult result; + private final Logger logger; + + public SCRAMServerSASL(SCRAM mechanism, Logger logger) throws NoSuchAlgorithmException { + this.mechanism = mechanism; + this.logger = logger; + this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), + UUID.randomUUID().toString()); + } + + @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); + Subject saslSubject = new Subject(false, Collections.singleton(new UserPrincipal(userName)), + Collections.singleton(userData), Collections.emptySet()); + result = new SCRAMSASLResult(userName, scram, saslSubject); + 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) { + logger.warn("SASL-SCRAM Authentication failed", e); + result = new SCRAMFailedSASLResult(); + } + return null; + } + + protected abstract UserData aquireUserData(String userName) throws LoginException; + + @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 index ec0f7ad95d5..ecd49791e1a 100644 --- 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 @@ -17,13 +17,8 @@ package org.apache.activemq.artemis.protocol.amqp.sasl.scram; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.util.Collections; import java.util.Iterator; -import java.util.UUID; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; @@ -36,7 +31,6 @@ 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.SASLResult; 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; @@ -46,7 +40,6 @@ 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.ScramException; import org.apache.activemq.artemis.spi.core.security.scram.UserData; import org.jboss.logging.Logger; @@ -78,11 +71,8 @@ public ServerSASL create(ActiveMQServer server, ProtocolManager RemotingConnection remotingConnection) { try { if (manager instanceof ProtonProtocolManager) { - ScramServerFunctionalityImpl scram = - new ScramServerFunctionalityImpl(scramType.getDigest(), scramType.getHmac(), - UUID.randomUUID().toString()); String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope(); - return new SCRAMServerSASL(scramType.getName(), scram, loginConfigScope, logger); + return new JAASSCRAMServerSASL(scramType, loginConfigScope, logger); } } catch (NoSuchAlgorithmException e) { // can't be used then... @@ -90,169 +80,58 @@ public ServerSASL create(ActiveMQServer server, ProtocolManager return null; } - private static final class SCRAMServerSASL implements ServerSASL { + private static final class JAASSCRAMServerSASL extends SCRAMServerSASL { - private final String name; - private final ScramServerFunctionality scram; - private SASLResult result; private final String loginConfigScope; - private final Logger logger; + private LoginContext loginContext = null; - SCRAMServerSASL(String name, ScramServerFunctionality scram, String loginConfigScope, Logger logger) { - this.name = name; - this.scram = scram; + JAASSCRAMServerSASL(SCRAM scram, String loginConfigScope, Logger logger) throws NoSuchAlgorithmException { + super(scram, logger); this.loginConfigScope = loginConfigScope; - this.logger = logger; } @Override - public String getName() { - return name; - } - - @Override - public byte[] processSASL(byte[] bytes) { - String message = new String(bytes, StandardCharsets.US_ASCII); - LoginContext loginContext = null; - try { - switch (scram.getState()) { - case INITIAL: { - String userName = scram.handleClientFirstMessage(message); - if (userName != null) { - - 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(name); - } 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(); - try { - Subject subject = loginContext.getSubject(); - Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); - Principal[] principals = subject.getPrincipals().toArray(new Principal[0]); - Subject saslSubject = new Subject(true, subject.getPrincipals(), subject.getPublicCredentials(), - Collections.singleton(principals)); - if (credentials.hasNext()) { - result = new SCRAMSASLResult(userName, scram, saslSubject); - String challenge = scram.prepareFirstMessage(credentials.next()); - return challenge.getBytes(StandardCharsets.US_ASCII); - } - } finally { - try { - loginContext.logout(); - } catch (LoginException e1) { - // we can't do anything useful then but also don'T want to fail here... - } - } + 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()); } - break; - } - case PREPARED_FIRST: { - String finalMessage = scram.prepareFinalMessage(message); - return finalMessage.getBytes(StandardCharsets.US_ASCII); - } - - default: - result = new SCRAMFailedSASLResult(); - break; - } - } catch (GeneralSecurityException | ScramException | RuntimeException e) { - logger.warn("SASL-SCRAM Authentication failed", e); - result = new SCRAMFailedSASLResult(); - if (loginContext != null) { - try { - loginContext.logout(); - } catch (LoginException e1) { - // we can't do anything useful then... } } + }); + loginContext.login(); + Subject subject = loginContext.getSubject(); + Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); + if (credentials.hasNext()) { + return credentials.next(); } - return null; - } - - @Override - public SASLResult result() { - if (result instanceof SCRAMSASLResult) { - return scram.isEnded() ? result : null; - } - return result; + throw new LoginException("can't aquire user data through configured login config scope (" + loginConfigScope + + ")"); } @Override public void done() { - } - - } - - 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"; + if (loginContext != null) { + try { + loginContext.logout(); + } catch (LoginException e1) { + // we can't do anything useful then... + } + loginContext = null; + } } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java index 80207897f08..68b6f8da8c2 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java @@ -29,7 +29,7 @@ public SHA1SCRAMServerSASLFactory() { @Override public int getPrecedence() { - return 100; + return 160; } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java index 26154c9787f..f7a13085046 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA256SCRAMServerSASLFactory.java @@ -29,6 +29,6 @@ public SHA256SCRAMServerSASLFactory() { @Override public int getPrecedence() { - return 200; + return 256; } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java new file mode 100644 index 00000000000..93c4008d70c --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA512SCRAMServerSASLFactory.java @@ -0,0 +1,34 @@ +/* + * 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 org.apache.activemq.artemis.spi.core.security.scram.SCRAM; + +/** + * provides SASL SRAM-SHA512 + */ +public class SHA512SCRAMServerSASLFactory extends SCRAMServerSASLFactory { + + public SHA512SCRAMServerSASLFactory() { + super(SCRAM.SHA512); + } + + @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 00000000000..1b8784432da --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionality.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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 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 + * @return true if the server's message is valid, false otherwise + */ + 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 00000000000..5c7bb99335c --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/ScramClientFunctionalityImpl.java @@ -0,0 +1,214 @@ +/* + * Copyright 2016 Ognyan Bankov + *

+ * 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 + *

+ * 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.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; + +/** + * 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 username 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/StringPrep.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java new file mode 100644 index 00000000000..d29c113cadd --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java @@ -0,0 +1,2138 @@ +/** + * Copyright 2011 Glenn Maynard + *

+ * 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 + *

+ * 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.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. + *

+ * 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 tupleStart = new ArrayList<>(); + private final ArrayList tupleCount = new ArrayList<>(); + + static CharClass fromList(int[] charMap) { + SortedMap 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 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 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 mappings) { + for (Map.Entry 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); + + // 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-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 366f9111f50..e7aaea9c45e 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 @@ -3,4 +3,5 @@ org.apache.activemq.artemis.protocol.amqp.sasl.PlainServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA1SCRAMServerSASLFactory -org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory \ No newline at end of file +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-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 index bac21f433bb..c55a2829485 100644 --- 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 @@ -20,8 +20,10 @@ * Defines sets of known SCRAM types with methods to fetch matching digest and hmac names */ public enum SCRAM { - SHA1, - SHA256; + // ordered by precedence + SHA512, + SHA256, + SHA1; public String getName() { switch (this) { @@ -29,6 +31,8 @@ public String getName() { return "SCRAM-SHA-1"; case SHA256: return "SCRAM-SHA-256"; + case SHA512: + return "SCRAM-SHA-512"; } throw new UnsupportedOperationException(); } @@ -39,6 +43,8 @@ public String getDigest() { return "SHA-1"; case SHA256: return "SHA-256"; + case SHA512: + return "SHA-512"; } throw new UnsupportedOperationException(); } @@ -49,6 +55,8 @@ public String getHmac() { return "HmacSHA1"; case SHA256: return "HmacSHA256"; + case SHA512: + return "HmacSHA512"; } throw new UnsupportedOperationException(); } 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 43c938d40c8..e8357eda98e 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,17 +17,29 @@ 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; import org.apache.qpid.proton.engine.Transport; +import org.jboss.logging.Logger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -107,7 +119,7 @@ public void testConnectsWithAnonymous() throws Exception { // 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 +147,8 @@ public void testConnectsWithPlain() throws Exception { }); // 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 +164,34 @@ public void testConnectsWithPlain() throws Exception { 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); + assertTrue(authenticator.succeeded()); + } + @Test(timeout = 20000) public void testConnectsWithExternal() throws Exception { doConnectWithExternalTestImpl(true); @@ -161,10 +202,13 @@ public void testExternalIgnoredWhenNoClientCertSupplied() throws Exception { 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 +235,17 @@ private void doConnectWithExternalTestImpl(boolean requireClientCert) throws Exe }); 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 +283,8 @@ private static byte[] expectedPlainInitialResponse(String username, String passw 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 +315,6 @@ public void process(Handler processComplete) { initialResponse = new byte[sasl.pending()]; sasl.recv(initialResponse, 0, initialResponse.length); - if (succeed) { sasl.done(SaslOutcome.PN_SASL_OK); } else { @@ -296,4 +342,92 @@ public byte[] getInitialResponse() { } } + private static final class SCRAMTestAuthenticator implements ProtonSaslAuthenticator { + + private final SCRAM mech; + private Sasl sasl; + private TestSCRAMServerSASL serverSASL; + + 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()); + try { + serverSASL = new TestSCRAMServerSASL(mech, Logger.getLogger(getClass())); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + + } + + @Override + public void process(Handler completionHandler) { + String[] remoteMechanisms = sasl.getRemoteMechanisms(); + int pending = sasl.pending(); + if (remoteMechanisms.length == 0 || pending == 0) { + completionHandler.handle(false); + return; + } + 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(); + System.out.println("end=" + ended); + 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(); + } + + } + + private static final class TestSCRAMServerSASL extends SCRAMServerSASL { + + TestSCRAMServerSASL(SCRAM mechanism, Logger logger) throws NoSuchAlgorithmException { + super(mechanism, logger); + } + + @Override + public void done() { + // nothing to do + } + + @Override + protected UserData aquireUserData(String userName) throws LoginException { + 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()); + } + } + + } + } From 6e52964a4c5f0847fb2530c2203dea6584e5c5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 24 Mar 2021 10:32:57 +0100 Subject: [PATCH 17/23] ARTEMIS-3106 - Support for SASL-SCRAM adjust examples and unit test --- .../amqp/sasl/scram/SCRAMServerSASL.java | 11 ++++++++--- .../amqp/sasl/scram/SCRAMServerSASLFactory.java | 17 ++++++++++++++--- .../core/security/jaas/JaasCallbackHandler.java | 13 +++++++++---- .../src/main/resources/artemis-users.properties | 5 +++-- .../resources/artemis-scram-users.properties | 6 +++--- 5 files changed, 37 insertions(+), 15 deletions(-) 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 index 456c77ea851..7da21180aaf 100644 --- 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 @@ -60,9 +60,7 @@ public byte[] processSASL(byte[] bytes) { case INITIAL: { String userName = scram.handleClientFirstMessage(message); UserData userData = aquireUserData(userName); - Subject saslSubject = new Subject(false, Collections.singleton(new UserPrincipal(userName)), - Collections.singleton(userData), Collections.emptySet()); - result = new SCRAMSASLResult(userName, scram, saslSubject); + result = new SCRAMSASLResult(userName, scram, createSaslSubject(userName, userData)); String challenge = scram.prepareFirstMessage(userData); return challenge.getBytes(StandardCharsets.US_ASCII); } @@ -83,6 +81,13 @@ public byte[] processSASL(byte[] bytes) { protected abstract UserData aquireUserData(String userName) throws LoginException; + 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) { 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 index ecd49791e1a..4526cf0782d 100644 --- 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 @@ -84,6 +84,7 @@ private static final class JAASSCRAMServerSASL extends SCRAMServerSASL { private final String loginConfigScope; private LoginContext loginContext = null; + private Subject loginSubject; JAASSCRAMServerSASL(SCRAM scram, String loginConfigScope, Logger logger) throws NoSuchAlgorithmException { super(scram, logger); @@ -113,8 +114,8 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback } }); loginContext.login(); - Subject subject = loginContext.getSubject(); - Iterator credentials = subject.getPublicCredentials(UserData.class).iterator(); + loginSubject = loginContext.getSubject(); + Iterator credentials = loginSubject.getPublicCredentials(UserData.class).iterator(); if (credentials.hasNext()) { return credentials.next(); } @@ -122,6 +123,15 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback ")"); } + @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) { @@ -130,8 +140,9 @@ public void done() { } catch (LoginException e1) { // we can't do anything useful then... } - loginContext = null; } + loginContext = null; + loginSubject = null; } } 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 0534243c054..e0c69893fa3 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 @@ -21,6 +21,7 @@ import java.io.IOException; import java.security.Principal; +import java.util.Set; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; @@ -73,17 +74,21 @@ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallback Subject peerSubject = remotingConnection.getSubject(); if (peerSubject != null) { - for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) { + for (KerberosPrincipal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) { principalsCallback.setPeerPrincipals(new Principal[] {principal}); return; } - for (Principal[] principals : peerSubject.getPrivateCredentials(Principal[].class)) { - principalsCallback.setPeerPrincipals(principals); + Set principals = peerSubject.getPrincipals(); + if (principals.size() > 0) { + principalsCallback.setPeerPrincipals(principals.toArray(new Principal[0])); return; } } - principalsCallback.setPeerPrincipals(new Principal[] {getPeerPrincipalFromConnection(remotingConnection)}); + Principal peerPrincipalFromConnection = getPeerPrincipalFromConnection(remotingConnection); + if (peerPrincipalFromConnection != null) { + principalsCallback.setPeerPrincipals(new Principal[] {peerPrincipalFromConnection}); + } } else { throw new UnsupportedCallbackException(callback); } 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 index fb524450712..48462355aef 100644 --- 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 @@ -17,7 +17,8 @@ ## # Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule [] -test|SHA1 = ENC(eI//JkJhVGYF8JX0ce8jCWkRE0BgNVuJ:4096:A/R04d4qd1y76W7FkWV67bhz68A=:p758FRkQekdJyLisYVBZiXMG0ac=) -test|SHA256 = ENC(pzWgKU7rGWuP6ZZpfqZdhXUw5OX+FMAy:4096:ArrBsIj8rLUPZh9yfHagQ5h7MfcxkkfguqyJTGRYKTg=:Ieqpx2kzV+VlZ26cIdTSoClmE8YyM3s8hx57LrZUr+Q=) +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/tests/integration-tests/src/test/resources/artemis-scram-users.properties b/tests/integration-tests/src/test/resources/artemis-scram-users.properties index babb223936d..23a2e36640e 100644 --- a/tests/integration-tests/src/test/resources/artemis-scram-users.properties +++ b/tests/integration-tests/src/test/resources/artemis-scram-users.properties @@ -16,11 +16,11 @@ ## --------------------------------------------------------------------------- # Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule [] -multi|SHA1 = ENC(qWyghLZUUGAsJxjDWy6iAvqVzb5br909:4096:bhM3xnt1XrPmPepxH+oy8YS0St0=:SpJQgsIxwYrbwb8Y5/QjuvWhEms=) -multi|SHA256 = ENC(qvzXWU1m8/9oyTKpomFmEfZNVTnCagmj:4096:5GPhT3fg5d5BJbMg0U8XBMXBvdc1YzKAM9j0jmSSd5k=:5X+WXPyhrgMQ3u0tuWDwDPtZdd+t/tFr01ccGkr2TK0=) +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(VeEFPtxYIGIS0VA7e+W5+LpdLmazpcLH:4096:rIPD9cVvOoVJqzD7u4/qRMW1xGwILRG90g2OtWmn8T0=:7k8fGWO1zWPuZF0pzDCYnDCtmaJxLptqHS26SNsbGHU=) +test = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=) From 66d386ca439f8af81eefb4d72995e65429667dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 24 Mar 2021 11:38:14 +0100 Subject: [PATCH 18/23] ARTEMIS-3106 - Support for SASL-SCRAM just use java Normalizer, android do not really matters here. --- .../protocol/amqp/sasl/scram/Normalizer.java | 87 ------------------- .../protocol/amqp/sasl/scram/StringPrep.java | 3 +- 2 files changed, 2 insertions(+), 88 deletions(-) delete mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java deleted file mode 100644 index 9e5f41b12e3..00000000000 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/Normalizer.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright 2011 Glenn Maynard - *

- * 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 - *

- * 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.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Dynamically-loaded interface for java.text.Normalizer(NFKC); it's missing in non-bleeding-edge - * versions of Android and we can get by without it. - */ -@SuppressWarnings({ "SpellCheckingInspection", "JavaDoc" }) -class Normalizer { - private static boolean initialized = false; - private static Method normalize; // java.text.Normalizer.normalize - private static Object nfkc; // java.text.Normalizer.Form.NFKC - private static final Object lock = new Object(); - - /** - * Equivalent to {@link java.text.Normalizer#normalize}(seq, Normalizer.Form.NFKC). If Normalizer - * is unavailable, returns the sequence unchanged. - */ - public static String normalize(CharSequence seq) { - synchronized (lock) { - if (!initialized) { - initialized = true; - initialize("java.text.Normalizer"); - } - } - - if (normalize == null) - return seq.toString(); - - try { - return (String) normalize.invoke(null, seq, nfkc); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("SameParameterValue") - private static void initialize(String classPath) { - try { - Class normalizerClass = Class.forName(classPath); - Class normalizerFormClass = findSubclassByName(normalizerClass); - Object[] normalizerConstants = normalizerFormClass.getEnumConstants(); - - nfkc = findObjectByValue(normalizerConstants, "NFKC"); - normalize = normalizerClass.getMethod("normalize", CharSequence.class, normalizerFormClass); - } catch (SecurityException | ClassNotFoundException | NoSuchMethodException e) { - throw new IllegalStateException("Couldn't load java.text.Normalizer", e); - } - } - - private static Class findSubclassByName(Class parentClass) throws ClassNotFoundException { - Class[] subClasses = parentClass.getClasses(); - String searchForName = parentClass.getName() + "$Form"; - for (Class memberClass : subClasses) { - String s = memberClass.getName(); - if (s.equals(searchForName)) - return memberClass; - } - throw new ClassNotFoundException(); - } - - @SuppressWarnings("SameParameterValue") - private static Object findObjectByValue(Object[] objects, String s) throws ClassNotFoundException { - for (Object e : objects) { - if (e.toString().equals(s)) - return e; - } - throw new ClassNotFoundException(); - } -} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java index d29c113cadd..13bb070cd58 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java @@ -15,6 +15,7 @@ */ package org.apache.activemq.artemis.protocol.amqp.sasl.scram; +import java.text.Normalizer; import java.util.ArrayList; import java.util.Collections; import java.util.Map; @@ -2118,7 +2119,7 @@ public static String prepAsQueryString(String s) throws StringPrepError { // 2) Normalize // rfc4013: 2.2. Normalization - s = Normalizer.normalize(s); + s = Normalizer.normalize(s, Normalizer.Form.NFKC); // 3) Prohibit int idx = containsCharacterInClass(s, saslProhibited); From e9dbbb6ca1a6cba05deff3be3df54380ea6155e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 24 Mar 2021 18:52:19 +0100 Subject: [PATCH 19/23] ARTEMIS-3106 - Support for SASL-SCRAM remove sysouts --- .../artemis/protocol/amqp/sasl/scram/SCRAMClientSASL.java | 3 --- 1 file changed, 3 deletions(-) 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 index 5f7ef218a3c..e7e1780e11e 100644 --- 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 @@ -59,7 +59,6 @@ public String getName() { public byte[] getInitialResponse() { try { String firstMessage = client.prepareFirstMessage(username); - System.out.println("SCRAMClientSASL.getInitialResponse() >> " + firstMessage); return firstMessage.getBytes(StandardCharsets.US_ASCII); } catch (ScramException e) { throw new DecodeException("prepareFirstMessage failed", e); @@ -69,11 +68,9 @@ public byte[] getInitialResponse() { @Override public byte[] getResponse(byte[] challenge) { String msg = new String(challenge, StandardCharsets.US_ASCII); - System.out.println("SCRAMClientSASL.getResponse() << " + msg); if (client.getState() == State.FIRST_PREPARED) { try { String finalMessage = client.prepareFinalMessage(password, msg); - System.out.println("SCRAMClientSASL.getResponse() >> " + finalMessage); return finalMessage.getBytes(StandardCharsets.US_ASCII); } catch (ScramException e) { throw new DecodeException("prepareFinalMessage failed", e); From 14b59e9cd4f75c1054dcf3f5109b28d9be08d392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Wed, 24 Mar 2021 19:08:15 +0100 Subject: [PATCH 20/23] ARTEMIS-3106 - Support for SASL-SCRAM check username as well --- .../tests/integration/amqp/connect/AMQPConnectSaslTest.java | 3 +++ 1 file changed, 3 insertions(+) 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 e8357eda98e..e018e6f4d29 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 @@ -415,6 +415,9 @@ public void done() { @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 { From b0bc523eb1605f5b190802ff599136f97e5952a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Thu, 25 Mar 2021 07:35:23 +0100 Subject: [PATCH 21/23] ARTEMIS-3106 - Support for SASL-SCRAM add unit-test / encode usernames --- .../amqp/sasl/scram/SCRAMClientSASL.java | 7 +- .../amqp/sasl/scram/SCRAMServerSASL.java | 16 +- .../sasl/scram/SCRAMServerSASLFactory.java | 9 +- .../scram/ScramClientFunctionalityImpl.java | 1 + .../scram/ScramServerFunctionalityImpl.java | 17 +- .../artemis/protocol/amqp/sasl/SCRAMTest.java | 192 ++++++++++++++++++ .../jaas/SCRAMPropertiesLoginModule.java | 39 ++-- .../spi/core/security}/scram/StringPrep.java | 2 +- .../amqp/connect/AMQPConnectSaslTest.java | 17 +- 9 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/SCRAMTest.java rename {artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl => artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security}/scram/StringPrep.java (99%) 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 index e7e1780e11e..2842e907c61 100644 --- 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 @@ -40,14 +40,19 @@ public class SCRAMClientSASL implements ClientSASL { * @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(), UUID.randomUUID().toString()); + client = new ScramClientFunctionalityImpl(scram.getDigest(), scram.getHmac(), nonce); } @Override 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 index 7da21180aaf..2c6826e2b38 100644 --- 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 @@ -31,20 +31,20 @@ 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; -import org.jboss.logging.Logger; public abstract class SCRAMServerSASL implements ServerSASL { protected final ScramServerFunctionality scram; protected final SCRAM mechanism; private SASLResult result; - private final Logger logger; - public SCRAMServerSASL(SCRAM mechanism, Logger logger) throws NoSuchAlgorithmException { + public SCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException { + this(mechanism, UUID.randomUUID().toString()); + } + + protected SCRAMServerSASL(SCRAM mechanism, String nonce) throws NoSuchAlgorithmException { this.mechanism = mechanism; - this.logger = logger; - this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), - UUID.randomUUID().toString()); + this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), nonce); } @Override @@ -73,14 +73,16 @@ public byte[] processSASL(byte[] bytes) { break; } } catch (GeneralSecurityException | ScramException | RuntimeException e) { - logger.warn("SASL-SCRAM Authentication failed", 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), 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 index 4526cf0782d..67ec428efdb 100644 --- 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 @@ -85,10 +85,12 @@ 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, logger); + super(scram); this.loginConfigScope = loginConfigScope; + this.logger = logger; } @Override @@ -145,6 +147,11 @@ public void done() { loginSubject = null; } + @Override + protected void failed(Exception e) { + logger.warn("SASL-SCRAM Authentication failed", e); + } + } } 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 index 5c7bb99335c..1f566e6cac1 100644 --- 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 @@ -30,6 +30,7 @@ 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 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 index 752eb400aa7..07cc0e16c9e 100644 --- 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 @@ -117,6 +117,14 @@ public String prepareFirstMessage(UserData userData) { @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()) { @@ -144,13 +152,8 @@ public String prepareFinalMessage(String clientFinalMessage) throws ScramExcepti } byte[] resultKey = digest.digest(clientKey); - if (!Arrays.equals(storedKeyArr, resultKey)) { - throw new ScramException("Nonce mismatch"); - } - - String result = "v=" + Base64.getEncoder().encodeToString(serverSignature); - mIsSuccessful = true; - return result; + mIsSuccessful = Arrays.equals(storedKeyArr, resultKey); + return "v=" + Base64.getEncoder().encodeToString(serverSignature); } @Override 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 00000000000..f646d6ef512 --- /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 data() { + List 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/SCRAMPropertiesLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/SCRAMPropertiesLoginModule.java index 1a61f0d01a8..7f52f58b354 100644 --- 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 @@ -38,9 +38,10 @@ 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; -import org.jgroups.util.UUID; /** * Login modules that uses properties files similar to the {@link PropertiesLoginModule}. It can @@ -81,18 +82,23 @@ public boolean login() throws LoginException { NameCallback nameCallback = new NameCallback("Username: "); executeCallbacks(nameCallback); user = nameCallback.getName(); - if (user == null) { - user = UUID.randomUUID().toString(); - } SCRAMMechanismCallback mechanismCallback = new SCRAMMechanismCallback(); executeCallbacks(mechanismCallback); SCRAM scram = getTypeByString(mechanismCallback.getMechanism()); - String password = users.getProperty(user, users.getProperty(user + SEPARATOR_MECHANISM + scram.name())); - 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]); + if (user == null) { + userData = generateUserData(null); // generate random user data } else { - userData = generateUserData(password); + 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; } @@ -176,8 +182,11 @@ public boolean logout() throws LoginException { * @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 { + public static void main(String[] args) throws GeneralSecurityException, ScramException, StringPrepError, + IOException { if (args.length < 2) { System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() + " []"); @@ -187,6 +196,8 @@ public static void main(String[] args) throws GeneralSecurityException, ScramExc } 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()); @@ -202,10 +213,12 @@ public static void main(String[] args) throws GeneralSecurityException, ScramExc } ScramUtils.NewPasswordStringData data = ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, iterations, digest, hmac)); - System.out.println(username + SEPARATOR_MECHANISM + scram.name() + " = " + - PasswordMaskingUtil.wrap(data.salt + SEPARATOR_PARAMETER + data.iterations + SEPARATOR_PARAMETER + - data.serverKey + SEPARATOR_PARAMETER + data.storedKey)); + 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) { diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java similarity index 99% rename from artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java rename to artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java index 13bb070cd58..8c7375216eb 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/StringPrep.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/scram/StringPrep.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.activemq.artemis.protocol.amqp.sasl.scram; +package org.apache.activemq.artemis.spi.core.security.scram; import java.text.Normalizer; import java.util.ArrayList; 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 e018e6f4d29..16ad0049636 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 @@ -39,7 +39,6 @@ import org.apache.qpid.proton.engine.Sasl; import org.apache.qpid.proton.engine.Sasl.SaslOutcome; import org.apache.qpid.proton.engine.Transport; -import org.jboss.logging.Logger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -359,7 +358,7 @@ public void init(NetSocket socket, ProtonConnection protonConnection, Transport sasl.allowSkip(false); sasl.setMechanisms(mech.getName()); try { - serverSASL = new TestSCRAMServerSASL(mech, Logger.getLogger(getClass())); + serverSASL = new TestSCRAMServerSASL(mech); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } @@ -381,7 +380,6 @@ public void process(Handler completionHandler) { sasl.send(result, 0, result.length); } boolean ended = serverSASL.isEnded(); - System.out.println("end=" + ended); if (ended) { if (succeeded()) { sasl.done(SaslOutcome.PN_SASL_OK); @@ -397,15 +395,17 @@ public void process(Handler completionHandler) { @Override public boolean succeeded() { SASLResult result = serverSASL.result(); - return result != null && result.isSuccess(); + return result != null && result.isSuccess() && serverSASL.e == null; } } private static final class TestSCRAMServerSASL extends SCRAMServerSASL { - TestSCRAMServerSASL(SCRAM mechanism, Logger logger) throws NoSuchAlgorithmException { - super(mechanism, logger); + private Exception e; + + TestSCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException { + super(mechanism); } @Override @@ -431,6 +431,11 @@ protected UserData aquireUserData(String userName) throws LoginException { } } + @Override + protected void failed(Exception e) { + this.e = e; + } + } } From ed6548993671a07d234cec0ba3cb0bd1c4057305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Mon, 29 Mar 2021 11:46:21 +0200 Subject: [PATCH 22/23] ARTEMIS-3106 - Support for SASL-SCRAM remove SHA1 --- .../scram/SHA1SCRAMServerSASLFactory.java | 35 ------------------- ...temis.protocol.amqp.sasl.ServerSASLFactory | 1 - .../spi/core/security/scram/SCRAM.java | 9 +---- 3 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java deleted file mode 100644 index 68b6f8da8c2..00000000000 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/scram/SHA1SCRAMServerSASLFactory.java +++ /dev/null @@ -1,35 +0,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. - */ -package org.apache.activemq.artemis.protocol.amqp.sasl.scram; - -import org.apache.activemq.artemis.spi.core.security.scram.SCRAM; - -/** - * provides SASL SCRAM-SHA1 - */ -public class SHA1SCRAMServerSASLFactory extends SCRAMServerSASLFactory { - - public SHA1SCRAMServerSASLFactory() { - super(SCRAM.SHA1); - } - - @Override - public int getPrecedence() { - return 160; - } - -} 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 e7aaea9c45e..c8f54d26369 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 @@ -2,6 +2,5 @@ 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 -org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA1SCRAMServerSASLFactory 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-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 index c55a2829485..1da4c42f9bd 100644 --- 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 @@ -22,13 +22,10 @@ public enum SCRAM { // ordered by precedence SHA512, - SHA256, - SHA1; + SHA256; public String getName() { switch (this) { - case SHA1: - return "SCRAM-SHA-1"; case SHA256: return "SCRAM-SHA-256"; case SHA512: @@ -39,8 +36,6 @@ public String getName() { public String getDigest() { switch (this) { - case SHA1: - return "SHA-1"; case SHA256: return "SHA-256"; case SHA512: @@ -51,8 +46,6 @@ public String getDigest() { public String getHmac() { switch (this) { - case SHA1: - return "HmacSHA1"; case SHA256: return "HmacSHA256"; case SHA512: From 58f80b1417f5b88dacc6fd2273d6403feae83cc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Mon, 29 Mar 2021 11:56:47 +0200 Subject: [PATCH 23/23] ARTEMIS-3106 - Support for SASL-SCRAM add more mechs --- .../tests/integration/amqp/connect/AMQPConnectSaslTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 16ad0049636..0a37e89c19a 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 @@ -188,7 +188,9 @@ public void testConnectsWithSCRAM() throws Exception { 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) @@ -346,6 +348,7 @@ private static final class SCRAMTestAuthenticator implements ProtonSaslAuthentic private final SCRAM mech; private Sasl sasl; private TestSCRAMServerSASL serverSASL; + private String chosenMech; SCRAMTestAuthenticator(SCRAM mech) { this.mech = mech; @@ -356,7 +359,7 @@ public void init(NetSocket socket, ProtonConnection protonConnection, Transport this.sasl = transport.sasl(); sasl.server(); sasl.allowSkip(false); - sasl.setMechanisms(mech.getName()); + sasl.setMechanisms(mech.getName(), PLAIN, ANONYMOUS); try { serverSASL = new TestSCRAMServerSASL(mech); } catch (NoSuchAlgorithmException e) { @@ -373,6 +376,7 @@ public void process(Handler completionHandler) { completionHandler.handle(false); return; } + chosenMech = remoteMechanisms[0]; byte[] msg = new byte[pending]; sasl.recv(msg, 0, msg.length); byte[] result = serverSASL.processSASL(msg);