From 33c44bd0d25c4fb082a48820e5b2ced20e29c0b2 Mon Sep 17 00:00:00 2001 From: Robert Godfrey Date: Sun, 6 Jul 2014 21:28:20 +0000 Subject: QPID-5878 : [Java Broker] Add SCRAM-SHA-256 SASL support git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@1608295 13f79535-47bb-0310-9956-ffa450edef68 --- .../manager/AbstractAuthenticationManager.java | 3 - .../AbstractScramAuthenticationManager.java | 397 +++++++++++++++++++++ .../security/auth/manager/ScramAuthUser.java | 4 +- .../manager/ScramSHA1AuthenticationManager.java | 349 +----------------- .../manager/ScramSHA256AuthenticationManager.java | 66 ++++ .../auth/sasl/scram/ScramSHA1SaslServer.java | 273 -------------- .../security/auth/sasl/scram/ScramSaslServer.java | 282 +++++++++++++++ .../java/resources/js/qpid/authorization/sasl.js | 36 +- .../src/main/java/resources/js/qpid/common/util.js | 2 +- .../security/CallbackHandlerRegistry.properties | 1 + .../security/DynamicSaslRegistrar.properties | 1 + .../security/scram/AbstractScramSaslClient.java | 350 ++++++++++++++++++ .../client/security/scram/ScramSHA1SaslClient.java | 309 +--------------- .../security/scram/ScramSHA1SaslClientFactory.java | 4 +- .../security/scram/ScramSHA256SaslClient.java | 34 ++ .../scram/ScramSHA256SaslClientFactory.java | 61 ++++ 16 files changed, 1236 insertions(+), 936 deletions(-) create mode 100644 java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractScramAuthenticationManager.java create mode 100644 java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA256AuthenticationManager.java delete mode 100644 java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java create mode 100644 java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSaslServer.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/scram/AbstractScramSaslClient.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClient.java create mode 100644 java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClientFactory.java (limited to 'java') diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java index 597b9e3bb1..a2bb5cb91a 100644 --- a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java @@ -26,7 +26,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import org.apache.log4j.Logger; @@ -148,8 +147,6 @@ public abstract class AbstractAuthenticationManager(attributes); - attributes.put(ConfiguredObject.ID, UUID.randomUUID()); - attributes.put(ConfiguredObject.DESIRED_STATE, State.ACTIVE); PreferencesProvider pp = getObjectFactory().create(PreferencesProvider.class, attributes, this); _preferencesProvider = pp; diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractScramAuthenticationManager.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractScramAuthenticationManager.java new file mode 100644 index 0000000000..f08c37008a --- /dev/null +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractScramAuthenticationManager.java @@ -0,0 +1,397 @@ +/* + * + * 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.qpid.server.security.auth.manager; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.login.AccountNotFoundException; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.xml.bind.DatatypeConverter; + +import org.apache.qpid.server.configuration.updater.Task; +import org.apache.qpid.server.configuration.updater.VoidTaskWithException; +import org.apache.qpid.server.model.Broker; +import org.apache.qpid.server.model.ConfiguredObject; +import org.apache.qpid.server.model.PasswordCredentialManagingAuthenticationProvider; +import org.apache.qpid.server.model.State; +import org.apache.qpid.server.model.User; +import org.apache.qpid.server.security.access.Operation; +import org.apache.qpid.server.security.auth.AuthenticationResult; +import org.apache.qpid.server.security.auth.UsernamePrincipal; +import org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServer; + +public abstract class AbstractScramAuthenticationManager> + extends AbstractAuthenticationManager + implements PasswordCredentialManagingAuthenticationProvider +{ + public static final String SCRAM_USER_TYPE = "scram"; + + static final Charset ASCII = Charset.forName("ASCII"); + private final SecureRandom _random = new SecureRandom(); + + private int _iterationCount = 4096; + + private Map _users = new ConcurrentHashMap(); + + + protected AbstractScramAuthenticationManager(final Map attributes, final Broker broker) + { + super(attributes, broker); + } + + @Override + public void initialise() + { + + } + + @Override + public String getMechanisms() + { + return getMechanismName(); + } + + protected abstract String getMechanismName(); + + @Override + public SaslServer createSaslServer(final String mechanism, + final String localFQDN, + final Principal externalPrincipal) + throws SaslException + { + return new ScramSaslServer(this, getMechanismName(), getHmacName(), getDigestName()); + } + + protected abstract String getDigestName(); + + @Override + public AuthenticationResult authenticate(final SaslServer server, final byte[] response) + { + try + { + // Process response from the client + byte[] challenge = server.evaluateResponse(response != null ? response : new byte[0]); + + if (server.isComplete()) + { + final String userId = server.getAuthorizationID(); + return new AuthenticationResult(new UsernamePrincipal(userId)); + } + else + { + return new AuthenticationResult(challenge, AuthenticationResult.AuthenticationStatus.CONTINUE); + } + } + catch (SaslException e) + { + return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e); + } + } + + @Override + public AuthenticationResult authenticate(final String username, final String password) + { + ScramAuthUser user = getUser(username); + if(user != null) + { + final String[] usernamePassword = user.getPassword().split(","); + byte[] salt = DatatypeConverter.parseBase64Binary(usernamePassword[0]); + try + { + if(Arrays.equals(DatatypeConverter.parseBase64Binary(usernamePassword[1]), + createSaltedPassword(salt, password))) + { + return new AuthenticationResult(new UsernamePrincipal(username)); + } + } + catch (SaslException e) + { + return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR,e); + } + + } + + return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR); + + + } + + + public int getIterationCount() + { + return _iterationCount; + } + + public byte[] getSalt(final String username) + { + ScramAuthUser user = getUser(username); + + if(user == null) + { + // don't disclose that the user doesn't exist, just generate random data so the failure is indistinguishable + // from the "wrong password" case + + byte[] salt = new byte[32]; + _random.nextBytes(salt); + return salt; + } + else + { + return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[0]); + } + } + + private static final byte[] INT_1 = new byte[]{0, 0, 0, 1}; + + public byte[] getSaltedPassword(final String username) throws SaslException + { + ScramAuthUser user = getUser(username); + if(user == null) + { + throw new SaslException("Authentication Failed"); + } + else + { + return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[1]); + } + } + + private ScramAuthUser getUser(final String username) + { + return _users.get(username); + } + + private byte[] createSaltedPassword(byte[] salt, String password) throws SaslException + { + Mac mac = createSha1Hmac(password.getBytes(ASCII)); + + mac.update(salt); + mac.update(INT_1); + byte[] result = mac.doFinal(); + + byte[] previous = null; + for(int i = 1; i < getIterationCount(); i++) + { + mac.update(previous != null? previous: result); + previous = mac.doFinal(); + for(int x = 0; x < result.length; x++) + { + result[x] ^= previous[x]; + } + } + + return result; + + } + + private Mac createSha1Hmac(final byte[] keyBytes) + throws SaslException + { + try + { + SecretKeySpec key = new SecretKeySpec(keyBytes, getHmacName()); + Mac mac = Mac.getInstance(getHmacName()); + mac.init(key); + return mac; + } + catch (NoSuchAlgorithmException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (InvalidKeyException e) + { + throw new SaslException(e.getMessage(), e); + } + } + + protected abstract String getHmacName(); + + @Override + public boolean createUser(final String username, final String password, final Map attributes) + { + return runTask(new Task() + { + @Override + public Boolean execute() + { + getSecurityManager().authoriseUserOperation(Operation.CREATE, username); + if (_users.containsKey(username)) + { + throw new IllegalArgumentException("User '" + username + "' already exists"); + } + try + { + Map userAttrs = new HashMap(); + userAttrs.put(User.ID, UUID.randomUUID()); + userAttrs.put(User.NAME, username); + userAttrs.put(User.PASSWORD, createStoredPassword(password)); + userAttrs.put(User.TYPE, SCRAM_USER_TYPE); + ScramAuthUser user = new ScramAuthUser(userAttrs, AbstractScramAuthenticationManager.this); + user.create(); + + return true; + } + catch (SaslException e) + { + throw new IllegalArgumentException(e); + } + } + }); + } + + org.apache.qpid.server.security.SecurityManager getSecurityManager() + { + return getBroker().getSecurityManager(); + } + + @Override + public void deleteUser(final String user) throws AccountNotFoundException + { + runTask(new VoidTaskWithException() + { + @Override + public void execute() throws AccountNotFoundException + { + final ScramAuthUser authUser = getUser(user); + if(authUser != null) + { + authUser.setState(State.DELETED); + } + else + { + throw new AccountNotFoundException("No such user: '" + user + "'"); + } + } + }); + } + + @Override + public void setPassword(final String username, final String password) throws AccountNotFoundException + { + runTask(new VoidTaskWithException() + { + @Override + public void execute() throws AccountNotFoundException + { + + final ScramAuthUser authUser = getUser(username); + if (authUser != null) + { + authUser.setPassword(password); + } + else + { + throw new AccountNotFoundException("No such user: '" + username + "'"); + } + } + }); + + } + + @Override + public Map> getUsers() + { + return runTask(new Task>>() + { + @Override + public Map> execute() + { + + Map> users = new HashMap>(); + for (String user : _users.keySet()) + { + users.put(user, Collections.emptyMap()); + } + return users; + } + }); + } + + @Override + public void reload() throws IOException + { + + } + + @Override + public void recoverUser(final User user) + { + _users.put(user.getName(), (ScramAuthUser) user); + } + + protected String createStoredPassword(final String password) throws SaslException + { + byte[] salt = new byte[32]; + _random.nextBytes(salt); + byte[] passwordBytes = createSaltedPassword(salt, password); + return DatatypeConverter.printBase64Binary(salt) + "," + DatatypeConverter.printBase64Binary(passwordBytes); + } + + @Override + public C addChild(final Class childClass, + final Map attributes, + final ConfiguredObject... otherParents) + { + if(childClass == User.class) + { + String username = (String) attributes.get("name"); + String password = (String) attributes.get("password"); + + if(createUser(username, password,null)) + { + @SuppressWarnings("unchecked") + C user = (C) _users.get(username); + return user; + } + else + { + return null; + + } + } + return super.addChild(childClass, attributes, otherParents); + } + + void doDeleted() + { + deleted(); + } + + Map getUserMap() + { + return _users; + } + +} diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramAuthUser.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramAuthUser.java index 15cd1d82a6..9a2d27f512 100644 --- a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramAuthUser.java +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramAuthUser.java @@ -44,12 +44,12 @@ import org.apache.qpid.server.security.access.Operation; class ScramAuthUser extends AbstractConfiguredObject implements User { - private ScramSHA1AuthenticationManager _authenticationManager; + private AbstractScramAuthenticationManager _authenticationManager; @ManagedAttributeField private String _password; @ManagedObjectFactoryConstructor - ScramAuthUser(final Map attributes, ScramSHA1AuthenticationManager parent) + ScramAuthUser(final Map attributes, AbstractScramAuthenticationManager parent) { super(parentsMap(parent), attributes); _authenticationManager = parent; diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java index 2138f4899e..6a48d29a71 100644 --- a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java @@ -20,53 +20,23 @@ */ package org.apache.qpid.server.security.auth.manager; -import java.io.IOException; import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.login.AccountNotFoundException; -import javax.security.sasl.SaslException; -import javax.security.sasl.SaslServer; -import javax.xml.bind.DatatypeConverter; - -import org.apache.qpid.server.configuration.updater.Task; -import org.apache.qpid.server.configuration.updater.VoidTaskWithException; import org.apache.qpid.server.model.Broker; -import org.apache.qpid.server.model.ConfiguredObject; import org.apache.qpid.server.model.ManagedObject; import org.apache.qpid.server.model.ManagedObjectFactoryConstructor; -import org.apache.qpid.server.model.PasswordCredentialManagingAuthenticationProvider; -import org.apache.qpid.server.model.State; -import org.apache.qpid.server.model.User; -import org.apache.qpid.server.security.SecurityManager; -import org.apache.qpid.server.security.access.Operation; -import org.apache.qpid.server.security.auth.AuthenticationResult; -import org.apache.qpid.server.security.auth.UsernamePrincipal; -import org.apache.qpid.server.security.auth.sasl.scram.ScramSHA1SaslServer; @ManagedObject( category = false, type = "SCRAM-SHA-1" ) public class ScramSHA1AuthenticationManager - extends AbstractAuthenticationManager - implements PasswordCredentialManagingAuthenticationProvider + extends AbstractScramAuthenticationManager { - public static final String SCRAM_USER_TYPE = "scram"; public static final String PROVIDER_TYPE = "SCRAM-SHA-1"; + private static final String HMAC_NAME = "HmacSHA1"; + static final Charset ASCII = Charset.forName("ASCII"); - public static final String HMAC_SHA_1 = "HmacSHA1"; - private final SecureRandom _random = new SecureRandom(); - private int _iterationCount = 4096; - private Map _users = new ConcurrentHashMap(); + private static final String MECHANISM = "SCRAM-SHA-1"; + private static final String DIGEST_NAME = "SHA-1"; @ManagedObjectFactoryConstructor @@ -76,318 +46,21 @@ public class ScramSHA1AuthenticationManager } @Override - public void initialise() - { - - } - - @Override - public String getMechanisms() - { - return ScramSHA1SaslServer.MECHANISM; - } - - @Override - public SaslServer createSaslServer(final String mechanism, - final String localFQDN, - final Principal externalPrincipal) - throws SaslException - { - return new ScramSHA1SaslServer(this); - } - - @Override - public AuthenticationResult authenticate(final SaslServer server, final byte[] response) - { - try - { - // Process response from the client - byte[] challenge = server.evaluateResponse(response != null ? response : new byte[0]); - - if (server.isComplete()) - { - final String userId = server.getAuthorizationID(); - return new AuthenticationResult(new UsernamePrincipal(userId)); - } - else - { - return new AuthenticationResult(challenge, AuthenticationResult.AuthenticationStatus.CONTINUE); - } - } - catch (SaslException e) - { - return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e); - } - } - - @Override - public AuthenticationResult authenticate(final String username, final String password) - { - ScramAuthUser user = getUser(username); - if(user != null) - { - final String[] usernamePassword = user.getPassword().split(","); - byte[] salt = DatatypeConverter.parseBase64Binary(usernamePassword[0]); - try - { - if(Arrays.equals(DatatypeConverter.parseBase64Binary(usernamePassword[1]),createSaltedPassword(salt, password))) - { - return new AuthenticationResult(new UsernamePrincipal(username)); - } - } - catch (SaslException e) - { - return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR,e); - } - - } - - return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR); - - - } - - - public int getIterationCount() - { - return _iterationCount; - } - - public byte[] getSalt(final String username) - { - ScramAuthUser user = getUser(username); - - if(user == null) - { - // don't disclose that the user doesn't exist, just generate random data so the failure is indistinguishable - // from the "wrong password" case - - byte[] salt = new byte[32]; - _random.nextBytes(salt); - return salt; - } - else - { - return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[0]); - } - } - - private static final byte[] INT_1 = new byte[]{0, 0, 0, 1}; - - public byte[] getSaltedPassword(final String username) throws SaslException - { - ScramAuthUser user = getUser(username); - if(user == null) - { - throw new SaslException("Authentication Failed"); - } - else - { - return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[1]); - } - } - - private ScramAuthUser getUser(final String username) - { - return _users.get(username); - } - - private byte[] createSaltedPassword(byte[] salt, String password) throws SaslException - { - Mac mac = createSha1Hmac(password.getBytes(ASCII)); - - mac.update(salt); - mac.update(INT_1); - byte[] result = mac.doFinal(); - - byte[] previous = null; - for(int i = 1; i < getIterationCount(); i++) - { - mac.update(previous != null? previous: result); - previous = mac.doFinal(); - for(int x = 0; x < result.length; x++) - { - result[x] ^= previous[x]; - } - } - - return result; - - } - - private Mac createSha1Hmac(final byte[] keyBytes) - throws SaslException - { - try - { - SecretKeySpec key = new SecretKeySpec(keyBytes, HMAC_SHA_1); - Mac mac = Mac.getInstance(HMAC_SHA_1); - mac.init(key); - return mac; - } - catch (NoSuchAlgorithmException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (InvalidKeyException e) - { - throw new SaslException(e.getMessage(), e); - } - } - - @Override - public boolean createUser(final String username, final String password, final Map attributes) - { - return runTask(new Task() - { - @Override - public Boolean execute() - { - getSecurityManager().authoriseUserOperation(Operation.CREATE, username); - if (_users.containsKey(username)) - { - throw new IllegalArgumentException("User '" + username + "' already exists"); - } - try - { - Map userAttrs = new HashMap(); - userAttrs.put(User.ID, UUID.randomUUID()); - userAttrs.put(User.NAME, username); - userAttrs.put(User.PASSWORD, createStoredPassword(password)); - userAttrs.put(User.TYPE, SCRAM_USER_TYPE); - ScramAuthUser user = new ScramAuthUser(userAttrs, ScramSHA1AuthenticationManager.this); - user.create(); - - return true; - } - catch (SaslException e) - { - throw new IllegalArgumentException(e); - } - } - }); - } - - SecurityManager getSecurityManager() - { - return getBroker().getSecurityManager(); - } - - @Override - public void deleteUser(final String user) throws AccountNotFoundException - { - runTask(new VoidTaskWithException() - { - @Override - public void execute() throws AccountNotFoundException - { - final ScramAuthUser authUser = getUser(user); - if(authUser != null) - { - authUser.setState(State.DELETED); - } - else - { - throw new AccountNotFoundException("No such user: '" + user + "'"); - } - } - }); - } - - @Override - public void setPassword(final String username, final String password) throws AccountNotFoundException + protected String getMechanismName() { - runTask(new VoidTaskWithException() - { - @Override - public void execute() throws AccountNotFoundException - { - - final ScramAuthUser authUser = getUser(username); - if (authUser != null) - { - authUser.setPassword(password); - } - else - { - throw new AccountNotFoundException("No such user: '" + username + "'"); - } - } - }); - - } - - @Override - public Map> getUsers() - { - return runTask(new Task>>() - { - @Override - public Map> execute() - { - - Map> users = new HashMap>(); - for (String user : _users.keySet()) - { - users.put(user, Collections.emptyMap()); - } - return users; - } - }); - } - - @Override - public void reload() throws IOException - { - + return MECHANISM; } @Override - public void recoverUser(final User user) - { - _users.put(user.getName(), (ScramAuthUser) user); - } - - protected String createStoredPassword(final String password) throws SaslException + protected String getDigestName() { - byte[] salt = new byte[32]; - _random.nextBytes(salt); - byte[] passwordBytes = createSaltedPassword(salt, password); - return DatatypeConverter.printBase64Binary(salt) + "," + DatatypeConverter.printBase64Binary(passwordBytes); + return DIGEST_NAME; } @Override - public C addChild(final Class childClass, - final Map attributes, - final ConfiguredObject... otherParents) + protected String getHmacName() { - if(childClass == User.class) - { - String username = (String) attributes.get("name"); - String password = (String) attributes.get("password"); - - if(createUser(username, password,null)) - { - @SuppressWarnings("unchecked") - C user = (C) _users.get(username); - return user; - } - else - { - return null; - - } - } - return super.addChild(childClass, attributes, otherParents); + return HMAC_NAME; } - void doDeleted() - { - deleted(); - } - - Map getUserMap() - { - return _users; - } } diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA256AuthenticationManager.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA256AuthenticationManager.java new file mode 100644 index 0000000000..ea5b82fdd5 --- /dev/null +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA256AuthenticationManager.java @@ -0,0 +1,66 @@ +/* + * + * 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.qpid.server.security.auth.manager; + +import java.nio.charset.Charset; +import java.util.Map; + +import org.apache.qpid.server.model.Broker; +import org.apache.qpid.server.model.ManagedObject; +import org.apache.qpid.server.model.ManagedObjectFactoryConstructor; + +@ManagedObject( category = false, type = "SCRAM-SHA-256" ) +public class ScramSHA256AuthenticationManager + extends AbstractScramAuthenticationManager +{ + public static final String PROVIDER_TYPE = "SCRAM-SHA-256"; + private static final String HMAC_NAME = "HmacSHA256"; + + static final Charset ASCII = Charset.forName("ASCII"); + private static final String MECHANISM = "SCRAM-SHA-256"; + private static final String DIGEST_NAME = "SHA-256"; + + + @ManagedObjectFactoryConstructor + protected ScramSHA256AuthenticationManager(final Map attributes, final Broker broker) + { + super(attributes, broker); + } + + @Override + protected String getMechanismName() + { + return MECHANISM; + } + + @Override + protected String getDigestName() + { + return DIGEST_NAME; + } + + @Override + protected String getHmacName() + { + return HMAC_NAME; + } + +} diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java deleted file mode 100644 index 71ef386e3e..0000000000 --- a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java +++ /dev/null @@ -1,273 +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.qpid.server.security.auth.sasl.scram; - -import org.apache.qpid.server.security.auth.manager.ScramSHA1AuthenticationManager; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.security.sasl.SaslException; -import javax.security.sasl.SaslServer; -import javax.xml.bind.DatatypeConverter; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.UUID; - -public class ScramSHA1SaslServer implements SaslServer -{ - public static final String MECHANISM = "SCRAM-SHA-1"; - - private static final Charset ASCII = Charset.forName("ASCII"); - - private final ScramSHA1AuthenticationManager _authManager; - private State _state = State.INITIAL; - private String _nonce; - private String _username; - private byte[] _gs2Header; - private String _serverFirstMessage; - private String _clientFirstMessageBare; - private byte[] _serverSignature; - - public ScramSHA1SaslServer(final ScramSHA1AuthenticationManager authenticationManager) - { - _authManager = authenticationManager; - } - - enum State - { - INITIAL, - SERVER_FIRST_MESSAGE_SENT, - COMPLETE - } - - @Override - public String getMechanismName() - { - return MECHANISM; - } - - @Override - public byte[] evaluateResponse(final byte[] response) throws SaslException - { - byte[] challenge; - switch (_state) - { - case INITIAL: - challenge = generateServerFirstMessage(response); - _state = State.SERVER_FIRST_MESSAGE_SENT; - break; - case SERVER_FIRST_MESSAGE_SENT: - challenge = generateServerFinalMessage(response); - _state = State.COMPLETE; - break; - default: - throw new SaslException("No response expected in state " + _state); - - } - return challenge; - } - - private byte[] generateServerFirstMessage(final byte[] response) throws SaslException - { - String clientFirstMessage = new String(response, ASCII); - if(!clientFirstMessage.startsWith("n")) - { - throw new SaslException("Cannot parse gs2-header"); - } - String[] parts = clientFirstMessage.split(","); - if(parts.length < 4) - { - throw new SaslException("Cannot parse client first message"); - } - _gs2Header = ("n,"+parts[1]+",").getBytes(ASCII); - _clientFirstMessageBare = clientFirstMessage.substring(_gs2Header.length); - if(!parts[2].startsWith("n=")) - { - throw new SaslException("Cannot parse client first message"); - } - _username = decodeUsername(parts[2].substring(2)); - if(!parts[3].startsWith("r=")) - { - throw new SaslException("Cannot parse client first message"); - } - _nonce = parts[3].substring(2) + UUID.randomUUID().toString(); - - int count = _authManager.getIterationCount(); - byte[] saltBytes = _authManager.getSalt(_username); - _serverFirstMessage = "r="+_nonce+",s="+ DatatypeConverter.printBase64Binary(saltBytes)+",i=" + count; - return _serverFirstMessage.getBytes(ASCII); - } - - private String decodeUsername(String username) throws SaslException - { - if(username.contains("=")) - { - String check = username; - while (check.contains("=")) - { - check = check.substring(check.indexOf('=') + 1); - if (!(check.startsWith("2C") || check.startsWith("3D"))) - { - throw new SaslException("Invalid username"); - } - } - username = username.replace("=2C", ","); - username = username.replace("=3D","="); - } - return username; - } - - - private byte[] generateServerFinalMessage(final byte[] response) throws SaslException - { - try - { - String clientFinalMessage = new String(response, ASCII); - String[] parts = clientFinalMessage.split(","); - if(!parts[0].startsWith("c=")) - { - throw new SaslException("Cannot parse client final message"); - } - if(!Arrays.equals(_gs2Header,DatatypeConverter.parseBase64Binary(parts[0].substring(2)))) - { - throw new SaslException("Client final message channel bind data invalid"); - } - if(!parts[1].startsWith("r=")) - { - throw new SaslException("Cannot parse client final message"); - } - if(!parts[1].substring(2).equals(_nonce)) - { - throw new SaslException("Client final message has incorrect nonce value"); - } - if(!parts[parts.length-1].startsWith("p=")) - { - throw new SaslException("Client final message does not have proof"); - } - - String clientFinalMessageWithoutProof = clientFinalMessage.substring(0,clientFinalMessage.length()-(1+parts[parts.length-1].length())); - byte[] proofBytes = DatatypeConverter.parseBase64Binary(parts[parts.length-1].substring(2)); - - String authMessage = _clientFirstMessageBare + "," + _serverFirstMessage + "," + clientFinalMessageWithoutProof; - - byte[] saltedPassword = _authManager.getSaltedPassword(_username); - - byte[] clientKey = computeHmacSHA1(saltedPassword, "Client Key"); - - byte[] storedKey = MessageDigest.getInstance("SHA1").digest(clientKey); - - byte[] clientSignature = computeHmacSHA1(storedKey, authMessage); - - byte[] clientProof = clientKey.clone(); - for(int i = 0 ; i < clientProof.length; i++) - { - clientProof[i] ^= clientSignature[i]; - } - - if(!Arrays.equals(clientProof, proofBytes)) - { - throw new SaslException("Authentication failed"); - } - byte[] serverKey = computeHmacSHA1(saltedPassword, "Server Key"); - String finalResponse = "v=" + DatatypeConverter.printBase64Binary(computeHmacSHA1(serverKey, authMessage)); - - return finalResponse.getBytes(ASCII); - } - catch (NoSuchAlgorithmException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (UnsupportedEncodingException e) - { - throw new SaslException(e.getMessage(), e); - } - } - - @Override - public boolean isComplete() - { - return _state == State.COMPLETE; - } - - @Override - public String getAuthorizationID() - { - return _username; - } - - @Override - public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws SaslException - { - throw new IllegalStateException("No security layer supported"); - } - - @Override - public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws SaslException - { - throw new IllegalStateException("No security layer supported"); - } - - @Override - public Object getNegotiatedProperty(final String propName) - { - return null; - } - - @Override - public void dispose() throws SaslException - { - - } - - private byte[] computeHmacSHA1(final byte[] key, final String string) - throws SaslException, UnsupportedEncodingException - { - Mac mac = createSha1Hmac(key); - mac.update(string.getBytes(ASCII)); - return mac.doFinal(); - } - - - private Mac createSha1Hmac(final byte[] keyBytes) - throws SaslException - { - try - { - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(key); - return mac; - } - catch (NoSuchAlgorithmException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (InvalidKeyException e) - { - throw new SaslException(e.getMessage(), e); - } - } - -} diff --git a/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSaslServer.java b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSaslServer.java new file mode 100644 index 0000000000..f510ec32d8 --- /dev/null +++ b/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSaslServer.java @@ -0,0 +1,282 @@ +/* + * + * 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.qpid.server.security.auth.sasl.scram; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import javax.xml.bind.DatatypeConverter; + +import org.apache.qpid.server.security.auth.manager.AbstractScramAuthenticationManager; + +public class ScramSaslServer implements SaslServer +{ + public final String _mechanism; + public final String _hmacName; + public final String _digestName; + + private static final Charset ASCII = Charset.forName("ASCII"); + + private final AbstractScramAuthenticationManager _authManager; + private State _state = State.INITIAL; + private String _nonce; + private String _username; + private byte[] _gs2Header; + private String _serverFirstMessage; + private String _clientFirstMessageBare; + private byte[] _serverSignature; + + public ScramSaslServer(final AbstractScramAuthenticationManager authenticationManager, + final String mechanism, + final String hmacName, + final String digestName) + { + _authManager = authenticationManager; + _mechanism = mechanism; + _hmacName = hmacName; + _digestName = digestName; + } + + enum State + { + INITIAL, + SERVER_FIRST_MESSAGE_SENT, + COMPLETE + } + + @Override + public String getMechanismName() + { + return _mechanism; + } + + @Override + public byte[] evaluateResponse(final byte[] response) throws SaslException + { + byte[] challenge; + switch (_state) + { + case INITIAL: + challenge = generateServerFirstMessage(response); + _state = State.SERVER_FIRST_MESSAGE_SENT; + break; + case SERVER_FIRST_MESSAGE_SENT: + challenge = generateServerFinalMessage(response); + _state = State.COMPLETE; + break; + default: + throw new SaslException("No response expected in state " + _state); + + } + return challenge; + } + + private byte[] generateServerFirstMessage(final byte[] response) throws SaslException + { + String clientFirstMessage = new String(response, ASCII); + if(!clientFirstMessage.startsWith("n")) + { + throw new SaslException("Cannot parse gs2-header"); + } + String[] parts = clientFirstMessage.split(","); + if(parts.length < 4) + { + throw new SaslException("Cannot parse client first message"); + } + _gs2Header = ("n,"+parts[1]+",").getBytes(ASCII); + _clientFirstMessageBare = clientFirstMessage.substring(_gs2Header.length); + if(!parts[2].startsWith("n=")) + { + throw new SaslException("Cannot parse client first message"); + } + _username = decodeUsername(parts[2].substring(2)); + if(!parts[3].startsWith("r=")) + { + throw new SaslException("Cannot parse client first message"); + } + _nonce = parts[3].substring(2) + UUID.randomUUID().toString(); + + int count = _authManager.getIterationCount(); + byte[] saltBytes = _authManager.getSalt(_username); + _serverFirstMessage = "r="+_nonce+",s="+ DatatypeConverter.printBase64Binary(saltBytes)+",i=" + count; + return _serverFirstMessage.getBytes(ASCII); + } + + private String decodeUsername(String username) throws SaslException + { + if(username.contains("=")) + { + String check = username; + while (check.contains("=")) + { + check = check.substring(check.indexOf('=') + 1); + if (!(check.startsWith("2C") || check.startsWith("3D"))) + { + throw new SaslException("Invalid username"); + } + } + username = username.replace("=2C", ","); + username = username.replace("=3D","="); + } + return username; + } + + + private byte[] generateServerFinalMessage(final byte[] response) throws SaslException + { + try + { + String clientFinalMessage = new String(response, ASCII); + String[] parts = clientFinalMessage.split(","); + if(!parts[0].startsWith("c=")) + { + throw new SaslException("Cannot parse client final message"); + } + if(!Arrays.equals(_gs2Header,DatatypeConverter.parseBase64Binary(parts[0].substring(2)))) + { + throw new SaslException("Client final message channel bind data invalid"); + } + if(!parts[1].startsWith("r=")) + { + throw new SaslException("Cannot parse client final message"); + } + if(!parts[1].substring(2).equals(_nonce)) + { + throw new SaslException("Client final message has incorrect nonce value"); + } + if(!parts[parts.length-1].startsWith("p=")) + { + throw new SaslException("Client final message does not have proof"); + } + + String clientFinalMessageWithoutProof = clientFinalMessage.substring(0,clientFinalMessage.length()-(1+parts[parts.length-1].length())); + byte[] proofBytes = DatatypeConverter.parseBase64Binary(parts[parts.length-1].substring(2)); + + String authMessage = _clientFirstMessageBare + "," + _serverFirstMessage + "," + clientFinalMessageWithoutProof; + + byte[] saltedPassword = _authManager.getSaltedPassword(_username); + + byte[] clientKey = computeHmac(saltedPassword, "Client Key"); + + byte[] storedKey = MessageDigest.getInstance(_digestName).digest(clientKey); + + byte[] clientSignature = computeHmac(storedKey, authMessage); + + byte[] clientProof = clientKey.clone(); + for(int i = 0 ; i < clientProof.length; i++) + { + clientProof[i] ^= clientSignature[i]; + } + + if(!Arrays.equals(clientProof, proofBytes)) + { + throw new SaslException("Authentication failed"); + } + byte[] serverKey = computeHmac(saltedPassword, "Server Key"); + String finalResponse = "v=" + DatatypeConverter.printBase64Binary(computeHmac(serverKey, authMessage)); + + return finalResponse.getBytes(ASCII); + } + catch (NoSuchAlgorithmException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (UnsupportedEncodingException e) + { + throw new SaslException(e.getMessage(), e); + } + } + + @Override + public boolean isComplete() + { + return _state == State.COMPLETE; + } + + @Override + public String getAuthorizationID() + { + return _username; + } + + @Override + public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws SaslException + { + throw new IllegalStateException("No security layer supported"); + } + + @Override + public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws SaslException + { + throw new IllegalStateException("No security layer supported"); + } + + @Override + public Object getNegotiatedProperty(final String propName) + { + return null; + } + + @Override + public void dispose() throws SaslException + { + + } + + private byte[] computeHmac(final byte[] key, final String string) + throws SaslException, UnsupportedEncodingException + { + Mac mac = createSha1Hmac(key); + mac.update(string.getBytes(ASCII)); + return mac.doFinal(); + } + + + private Mac createSha1Hmac(final byte[] keyBytes) + throws SaslException + { + try + { + SecretKeySpec key = new SecretKeySpec(keyBytes, _hmacName); + Mac mac = Mac.getInstance(_hmacName); + mac.init(key); + return mac; + } + catch (NoSuchAlgorithmException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (InvalidKeyException e) + { + throw new SaslException(e.getMessage(), e); + } + } + +} diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js index c00f0eae19..82404d100c 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js @@ -138,10 +138,20 @@ var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFu }; - var saslScramSha1 = function saslScramSha1(user, password, saslMechanism, callbackFunction) - { + var saslScramSha1 = function saslScramSha1(user, password, saslMechanism, callbackFunction) { + saslScram("sha1",user,password,saslMechanism,callbackFunction); + }; + + var saslScramSha256 = function saslScramSha1(user, password, saslMechanism, callbackFunction) { + saslScram("sha256",user,password,saslMechanism,callbackFunction); + }; + + var saslScram = function saslScramSha1(mechanism, user, password, saslMechanism, callbackFunction) { - script.get("webjars/cryptojs/3.1.2/rollups/hmac-sha1.js").then( function() + var DIGEST = mechanism.toUpperCase(); + var HMAC = "Hmac"+DIGEST; + + script.get("webjars/cryptojs/3.1.2/rollups/hmac-"+mechanism+".js").then( function() { script.get("webjars/cryptojs/3.1.2/components/enc-base64-min.js").then ( function() { @@ -187,7 +197,7 @@ var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFu var generateSaltedPassword = function generateSaltedPassword(salt, password, iterationCount) { - var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, password); + var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo[DIGEST], password); hmac.update(salt); hmac.update(CryptoJS.enc.Hex.parse("00000001")); @@ -196,7 +206,7 @@ var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFu var previous = null; for(var i = 1 ;i < iterationCount; i++) { - hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, password); + hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo[DIGEST], password); hmac.update( previous != null ? previous : result ); previous = hmac.finalize(); result = xor(result, previous); @@ -238,12 +248,12 @@ var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFu var saltedPassword = generateSaltedPassword(salt, password, iterationCount) var clientFinalMessageWithoutProof = "c=" + toBase64(GS2_HEADER) + ",r=" + nonce; var authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof; - var clientKey = CryptoJS.HmacSHA1("Client Key", saltedPassword); - var storedKey = CryptoJS.SHA1(clientKey); - var clientSignature = CryptoJS.HmacSHA1(authMessage, storedKey); + var clientKey = CryptoJS[HMAC]("Client Key", saltedPassword); + var storedKey = CryptoJS[DIGEST](clientKey); + var clientSignature = CryptoJS[HMAC](authMessage, storedKey); var clientProof = xor(clientKey, clientSignature); - var serverKey = CryptoJS.HmacSHA1("Server Key", saltedPassword); - serverSignature = CryptoJS.HmacSHA1(authMessage, serverKey); + var serverKey = CryptoJS[HMAC]("Server Key", saltedPassword); + serverSignature = CryptoJS[HMAC](authMessage, serverKey); dojo.xhrPost({ // The URL of the request url: saslServiceUrl, @@ -300,7 +310,11 @@ SaslClient.authenticate = function(username, password, callbackFunction) }).then(function(data) { var mechMap = data.mechanisms; - if(containsMechanism(mechMap, "SCRAM-SHA-1")) + if(containsMechanism(mechMap, "SCRAM-SHA-256")) + { + saslScramSha256(username, password, "SCRAM-SHA-256", callbackFunction) + } + else if(containsMechanism(mechMap, "SCRAM-SHA-1")) { saslScramSha1(username, password, "SCRAM-SHA-1", callbackFunction) } diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js index 46d0cfa35d..1391d7d5ff 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js @@ -140,7 +140,7 @@ define(["dojo/_base/xhr", util.isProviderManagingUsers = function(type) { - return (type === "PlainPasswordFile" || type === "Base64MD5PasswordFile" || type === "SCRAM-SHA-1"); + return (type === "PlainPasswordFile" || type === "Base64MD5PasswordFile" || type === "SCRAM-SHA-1" || type === "SCRAM-SHA-256"); }; util.showSetAttributesDialog = function(attributeWidgetFactories, data, putURL, dialogTitle, appendNameToUrl) diff --git a/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties index 7573664187..8f02ee2c38 100644 --- a/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties +++ b/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties @@ -32,4 +32,5 @@ AMQPLAIN.5=org.apache.qpid.client.security.UsernamePasswordCallbackHandler PLAIN.6=org.apache.qpid.client.security.UsernamePasswordCallbackHandler ANONYMOUS.7=org.apache.qpid.client.security.UsernamePasswordCallbackHandler SCRAM-SHA-1.8=org.apache.qpid.client.security.UsernamePasswordCallbackHandler +SCRAM-SHA-256.9=org.apache.qpid.client.security.UsernamePasswordCallbackHandler diff --git a/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties index fd52935fe5..24a76982ef 100644 --- a/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties +++ b/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties @@ -20,3 +20,4 @@ AMQPLAIN=org.apache.qpid.client.security.amqplain.AmqPlainSaslClientFactory CRAM-MD5-HASHED=org.apache.qpid.client.security.crammd5hashed.CRAMMD5HashedSaslClientFactory ANONYMOUS=org.apache.qpid.client.security.anonymous.AnonymousSaslClientFactory SCRAM-SHA-1=org.apache.qpid.client.security.scram.ScramSHA1SaslClientFactory +SCRAM-SHA-256=org.apache.qpid.client.security.scram.ScramSHA256SaslClientFactory diff --git a/java/client/src/main/java/org/apache/qpid/client/security/scram/AbstractScramSaslClient.java b/java/client/src/main/java/org/apache/qpid/client/security/scram/AbstractScramSaslClient.java new file mode 100644 index 0000000000..1e67567b8b --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/scram/AbstractScramSaslClient.java @@ -0,0 +1,350 @@ +/* + * + * 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.qpid.client.security.scram; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import javax.xml.bind.DatatypeConverter; + +public abstract class AbstractScramSaslClient implements SaslClient +{ + + private static final byte[] INT_1 = new byte[]{0, 0, 0, 1}; + private static final String GS2_HEADER = "n,,"; + private static final Charset ASCII = Charset.forName("ASCII"); + + private final String _digestName; + private final String _hmacName; + + private String _username; + private final String _clientNonce = UUID.randomUUID().toString(); + private String _serverNonce; + private byte[] _salt; + private int _iterationCount; + private String _clientFirstMessageBare; + private byte[] _serverSignature; + + enum State + { + INITIAL, + CLIENT_FIRST_SENT, + CLIENT_PROOF_SENT, + COMPLETE + } + + public final String _mechanism; + + private final CallbackHandler _callbackHandler; + + private State _state = State.INITIAL; + + public AbstractScramSaslClient(final CallbackHandler cbh, + final String mechanism, + final String digestName, + final String hmacName) + { + _callbackHandler = cbh; + _mechanism = mechanism; + _digestName = digestName; + _hmacName = hmacName; + + } + + @Override + public String getMechanismName() + { + return _mechanism; + } + + @Override + public boolean hasInitialResponse() + { + return true; + } + + @Override + public byte[] evaluateChallenge(final byte[] challenge) throws SaslException + { + byte[] response; + switch(_state) + { + case INITIAL: + response = initialResponse(); + _state = State.CLIENT_FIRST_SENT; + break; + case CLIENT_FIRST_SENT: + response = calculateClientProof(challenge); + _state = State.CLIENT_PROOF_SENT; + break; + case CLIENT_PROOF_SENT: + evaluateOutcome(challenge); + response = null; + _state = State.COMPLETE; + break; + default: + throw new SaslException("No challenge expected in state " + _state); + } + return response; + } + + private void evaluateOutcome(final byte[] challenge) throws SaslException + { + String serverFinalMessage = new String(challenge, ASCII); + String[] parts = serverFinalMessage.split(","); + if(!parts[0].startsWith("v=")) + { + throw new SaslException("Server final message did not contain verifier"); + } + byte[] serverSignature = DatatypeConverter.parseBase64Binary(parts[0].substring(2)); + if(!Arrays.equals(_serverSignature, serverSignature)) + { + throw new SaslException("Server signature did not match"); + } + } + + private byte[] calculateClientProof(final byte[] challenge) throws SaslException + { + try + { + String serverFirstMessage = new String(challenge, ASCII); + String[] parts = serverFirstMessage.split(","); + if(parts.length < 3) + { + throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed"); + } + else if(parts[0].startsWith("m=")) + { + throw new SaslException("Server requires mandatory extension which is not supported: " + parts[0]); + } + else if(!parts[0].startsWith("r=")) + { + throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find nonce"); + } + String nonce = parts[0].substring(2); + if(!nonce.startsWith(_clientNonce)) + { + throw new SaslException("Server challenge did not use correct client nonce"); + } + _serverNonce = nonce; + if(!parts[1].startsWith("s=")) + { + throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find salt"); + } + String base64Salt = parts[1].substring(2); + _salt = DatatypeConverter.parseBase64Binary(base64Salt); + if(!parts[2].startsWith("i=")) + { + throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find iteration count"); + } + String iterCountString = parts[2].substring(2); + _iterationCount = Integer.parseInt(iterCountString); + if(_iterationCount <= 0) + { + throw new SaslException("Iteration count " + _iterationCount + " is not a positive integer"); + } + PasswordCallback passwordCallback = new PasswordCallback("Password", false); + _callbackHandler.handle(new Callback[] { passwordCallback }); + byte[] passwordBytes = saslPrep(new String(passwordCallback.getPassword())).getBytes("UTF-8"); + + byte[] saltedPassword = generateSaltedPassword(passwordBytes); + + + String clientFinalMessageWithoutProof = + "c=" + DatatypeConverter.printBase64Binary(GS2_HEADER.getBytes(ASCII)) + + ",r=" + _serverNonce; + + String authMessage = _clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof; + + byte[] clientKey = computeHmac(saltedPassword, "Client Key"); + byte[] storedKey = MessageDigest.getInstance(_digestName).digest(clientKey); + + byte[] clientSignature = computeHmac(storedKey, authMessage); + + byte[] clientProof = clientKey.clone(); + for(int i = 0 ; i < clientProof.length; i++) + { + clientProof[i] ^= clientSignature[i]; + } + byte[] serverKey = computeHmac(saltedPassword, "Server Key"); + _serverSignature = computeHmac(serverKey, authMessage); + + String finalMessageWithProof = clientFinalMessageWithoutProof + + ",p=" + DatatypeConverter.printBase64Binary(clientProof); + return finalMessageWithProof.getBytes(); + } + catch (UnsupportedEncodingException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (IllegalArgumentException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (UnsupportedCallbackException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (IOException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (NoSuchAlgorithmException e) + { + throw new SaslException(e.getMessage(), e); + } + } + + private byte[] computeHmac(final byte[] key, final String string) + throws SaslException, UnsupportedEncodingException + { + Mac mac = createHmac(key); + mac.update(string.getBytes(ASCII)); + return mac.doFinal(); + } + + private byte[] generateSaltedPassword(final byte[] passwordBytes) throws SaslException + { + Mac mac = createHmac(passwordBytes); + + mac.update(_salt); + mac.update(INT_1); + byte[] result = mac.doFinal(); + + byte[] previous = null; + for(int i = 1; i < _iterationCount; i++) + { + mac.update(previous != null? previous: result); + previous = mac.doFinal(); + for(int x = 0; x < result.length; x++) + { + result[x] ^= previous[x]; + } + } + + return result; + } + + private Mac createHmac(final byte[] keyBytes) + throws SaslException + { + try + { + SecretKeySpec key = new SecretKeySpec(keyBytes, _hmacName); + Mac mac = Mac.getInstance(_hmacName); + mac.init(key); + return mac; + } + catch (NoSuchAlgorithmException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (InvalidKeyException e) + { + throw new SaslException(e.getMessage(), e); + } + } + + + private byte[] initialResponse() throws SaslException + { + try + { + StringBuffer buf = new StringBuffer("n="); + NameCallback nameCallback = new NameCallback("Username?"); + _callbackHandler.handle(new Callback[] { nameCallback }); + _username = nameCallback.getName(); + buf.append(saslPrep(_username)); + buf.append(",r="); + buf.append(_clientNonce); + _clientFirstMessageBare = buf.toString(); + return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII); + } + catch (UnsupportedCallbackException e) + { + throw new SaslException(e.getMessage(), e); + } + catch (IOException e) + { + throw new SaslException(e.getMessage(), e); + } + } + + private String saslPrep(String name) throws SaslException + { + // TODO - a real implementation of SaslPrep + + if(!ASCII.newEncoder().canEncode(name)) + { + throw new SaslException("Can only encode names and passwords which are restricted to ASCII characters"); + } + + name = name.replace("=", "=3D"); + name = name.replace(",", "=2C"); + return name; + } + + @Override + public boolean isComplete() + { + return _state == State.COMPLETE; + } + + @Override + public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws SaslException + { + throw new IllegalStateException("No security layer supported"); + } + + @Override + public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws SaslException + { + throw new IllegalStateException("No security layer supported"); + } + + @Override + public Object getNegotiatedProperty(final String propName) + { + return null; + } + + @Override + public void dispose() throws SaslException + { + + } + +} diff --git a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java index 91c03e18c1..b6704e9d94 100644 --- a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java +++ b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClient.java @@ -20,320 +20,15 @@ */ package org.apache.qpid.client.security.scram; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.sasl.SaslClient; -import javax.security.sasl.SaslException; -import javax.xml.bind.DatatypeConverter; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.UUID; -public class ScramSHA1SaslClient implements SaslClient +public class ScramSHA1SaslClient extends AbstractScramSaslClient { - private static final byte[] INT_1 = new byte[]{0, 0, 0, 1}; - private static final String GS2_HEADER = "n,,"; - private static final Charset ASCII = Charset.forName("ASCII"); - - private String _username; - private final String _clientNonce = UUID.randomUUID().toString(); - private String _serverNonce; - private byte[] _salt; - private int _iterationCount; - private String _clientFirstMessageBare; - private byte[] _serverSignature; - - enum State - { - INITIAL, - CLIENT_FIRST_SENT, - CLIENT_PROOF_SENT, - COMPLETE - } - public static final String MECHANISM = "SCRAM-SHA-1"; - private final CallbackHandler _callbackHandler; - - private State _state = State.INITIAL; - public ScramSHA1SaslClient(final CallbackHandler cbh) { - _callbackHandler = cbh; - } - - @Override - public String getMechanismName() - { - return MECHANISM; - } - - @Override - public boolean hasInitialResponse() - { - return true; - } - - @Override - public byte[] evaluateChallenge(final byte[] challenge) throws SaslException - { - byte[] response; - switch(_state) - { - case INITIAL: - response = initialResponse(); - _state = State.CLIENT_FIRST_SENT; - break; - case CLIENT_FIRST_SENT: - response = calculateClientProof(challenge); - _state = State.CLIENT_PROOF_SENT; - break; - case CLIENT_PROOF_SENT: - evaluateOutcome(challenge); - response = null; - _state = State.COMPLETE; - break; - default: - throw new SaslException("No challenge expected in state " + _state); - } - return response; - } - - private void evaluateOutcome(final byte[] challenge) throws SaslException - { - String serverFinalMessage = new String(challenge, ASCII); - String[] parts = serverFinalMessage.split(","); - if(!parts[0].startsWith("v=")) - { - throw new SaslException("Server final message did not contain verifier"); - } - byte[] serverSignature = DatatypeConverter.parseBase64Binary(parts[0].substring(2)); - if(!Arrays.equals(_serverSignature, serverSignature)) - { - throw new SaslException("Server signature did not match"); - } - } - - private byte[] calculateClientProof(final byte[] challenge) throws SaslException - { - try - { - String serverFirstMessage = new String(challenge, ASCII); - String[] parts = serverFirstMessage.split(","); - if(parts.length < 3) - { - throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed"); - } - else if(parts[0].startsWith("m=")) - { - throw new SaslException("Server requires mandatory extension which is not supported: " + parts[0]); - } - else if(!parts[0].startsWith("r=")) - { - throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find nonce"); - } - String nonce = parts[0].substring(2); - if(!nonce.startsWith(_clientNonce)) - { - throw new SaslException("Server challenge did not use correct client nonce"); - } - _serverNonce = nonce; - if(!parts[1].startsWith("s=")) - { - throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find salt"); - } - String base64Salt = parts[1].substring(2); - _salt = DatatypeConverter.parseBase64Binary(base64Salt); - if(!parts[2].startsWith("i=")) - { - throw new SaslException("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find iteration count"); - } - String iterCountString = parts[2].substring(2); - _iterationCount = Integer.parseInt(iterCountString); - if(_iterationCount <= 0) - { - throw new SaslException("Iteration count " + _iterationCount + " is not a positive integer"); - } - PasswordCallback passwordCallback = new PasswordCallback("Password", false); - _callbackHandler.handle(new Callback[] { passwordCallback }); - byte[] passwordBytes = saslPrep(new String(passwordCallback.getPassword())).getBytes("UTF-8"); - - byte[] saltedPassword = generateSaltedPassword(passwordBytes); - - - String clientFinalMessageWithoutProof = - "c=" + DatatypeConverter.printBase64Binary(GS2_HEADER.getBytes(ASCII)) - + ",r=" + _serverNonce; - - String authMessage = _clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof; - - byte[] clientKey = computeHmacSHA1(saltedPassword, "Client Key"); - byte[] storedKey = MessageDigest.getInstance("SHA1").digest(clientKey); - - byte[] clientSignature = computeHmacSHA1(storedKey, authMessage); - - byte[] clientProof = clientKey.clone(); - for(int i = 0 ; i < clientProof.length; i++) - { - clientProof[i] ^= clientSignature[i]; - } - byte[] serverKey = computeHmacSHA1(saltedPassword, "Server Key"); - _serverSignature = computeHmacSHA1(serverKey, authMessage); - - String finalMessageWithProof = clientFinalMessageWithoutProof - + ",p=" + DatatypeConverter.printBase64Binary(clientProof); - return finalMessageWithProof.getBytes(); - } - catch (UnsupportedEncodingException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (IllegalArgumentException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (UnsupportedCallbackException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (IOException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (NoSuchAlgorithmException e) - { - throw new SaslException(e.getMessage(), e); - } - } - - private byte[] computeHmacSHA1(final byte[] key, final String string) - throws SaslException, UnsupportedEncodingException - { - Mac mac = createSha1Hmac(key); - mac.update(string.getBytes(ASCII)); - return mac.doFinal(); - } - - private byte[] generateSaltedPassword(final byte[] passwordBytes) throws SaslException - { - Mac mac = createSha1Hmac(passwordBytes); - - mac.update(_salt); - mac.update(INT_1); - byte[] result = mac.doFinal(); - - byte[] previous = null; - for(int i = 1; i < _iterationCount; i++) - { - mac.update(previous != null? previous: result); - previous = mac.doFinal(); - for(int x = 0; x < result.length; x++) - { - result[x] ^= previous[x]; - } - } - - return result; - } - - private Mac createSha1Hmac(final byte[] keyBytes) - throws SaslException - { - try - { - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1"); - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(key); - return mac; - } - catch (NoSuchAlgorithmException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (InvalidKeyException e) - { - throw new SaslException(e.getMessage(), e); - } - } - - - private byte[] initialResponse() throws SaslException - { - try - { - StringBuffer buf = new StringBuffer("n="); - NameCallback nameCallback = new NameCallback("Username?"); - _callbackHandler.handle(new Callback[] { nameCallback }); - _username = nameCallback.getName(); - buf.append(saslPrep(_username)); - buf.append(",r="); - buf.append(_clientNonce); - _clientFirstMessageBare = buf.toString(); - return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII); - } - catch (UnsupportedCallbackException e) - { - throw new SaslException(e.getMessage(), e); - } - catch (IOException e) - { - throw new SaslException(e.getMessage(), e); - } - } - - private String saslPrep(String name) throws SaslException - { - // TODO - a real implementation of SaslPrep - - if(!ASCII.newEncoder().canEncode(name)) - { - throw new SaslException("Can only encode names and passwords which are restricted to ASCII characters"); - } - - name = name.replace("=", "=3D"); - name = name.replace(",", "=2C"); - return name; - } - - @Override - public boolean isComplete() - { - return _state == State.COMPLETE; - } - - @Override - public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws SaslException - { - throw new IllegalStateException("No security layer supported"); - } - - @Override - public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws SaslException - { - throw new IllegalStateException("No security layer supported"); + super(cbh, MECHANISM, "SHA-1", "HmacSHA1"); } - - @Override - public Object getNegotiatedProperty(final String propName) - { - return null; - } - - @Override - public void dispose() throws SaslException - { - - } - } diff --git a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java index 8a54abbc04..59ef236bde 100644 --- a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java +++ b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA1SaslClientFactory.java @@ -20,14 +20,16 @@ */ package org.apache.qpid.client.security.scram; +import java.util.Map; + import javax.security.auth.callback.CallbackHandler; import javax.security.sasl.SaslClient; import javax.security.sasl.SaslClientFactory; import javax.security.sasl.SaslException; -import java.util.Map; public class ScramSHA1SaslClientFactory implements SaslClientFactory { + @Override public SaslClient createSaslClient(final String[] mechanisms, final String authorizationId, diff --git a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClient.java b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClient.java new file mode 100644 index 0000000000..8779c36f0d --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClient.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.qpid.client.security.scram; + +import javax.security.auth.callback.CallbackHandler; + +public class ScramSHA256SaslClient extends AbstractScramSaslClient +{ + + public static final String MECHANISM = "SCRAM-SHA-256"; + + public ScramSHA256SaslClient(final CallbackHandler cbh) + { + super(cbh, MECHANISM, "SHA-256", "HmacSHA256"); + } +} diff --git a/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClientFactory.java b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClientFactory.java new file mode 100644 index 0000000000..fff762f8ba --- /dev/null +++ b/java/client/src/main/java/org/apache/qpid/client/security/scram/ScramSHA256SaslClientFactory.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.qpid.client.security.scram; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslClientFactory; +import javax.security.sasl.SaslException; + +public class ScramSHA256SaslClientFactory implements SaslClientFactory +{ + + @Override + public SaslClient createSaslClient(final String[] mechanisms, + final String authorizationId, + final String protocol, + final String serverName, + final Map props, + final CallbackHandler cbh) throws SaslException + { + for (int i = 0; i < mechanisms.length; i++) + { + if (mechanisms[i].equals(ScramSHA256SaslClient.MECHANISM)) + { + if (cbh == null) + { + throw new SaslException("CallbackHandler must not be null"); + } + return new ScramSHA256SaslClient(cbh); + } + + } + return null; + } + + @Override + public String[] getMechanismNames(final Map props) + { + return new String[0]; + } +} -- cgit v1.2.1