blob: 93f946dbc89d5eccece47e5545ac957b63df60e3 [file] [log] [blame]
/*
*
* 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.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
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 com.google.common.util.concurrent.ListenableFuture;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.ManagedContextDefault;
import org.apache.qpid.server.model.PasswordCredentialManagingAuthenticationProvider;
import org.apache.qpid.server.model.State;
import org.apache.qpid.server.model.StateTransition;
import org.apache.qpid.server.security.auth.AuthenticationResult;
import org.apache.qpid.server.security.auth.UsernamePrincipal;
import org.apache.qpid.server.security.auth.sasl.plain.PlainAdapterSaslServer;
import org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServer;
import org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServerSource;
public abstract class AbstractScramAuthenticationManager<X extends AbstractScramAuthenticationManager<X>>
extends ConfigModelPasswordManagingAuthenticationProvider<X>
implements PasswordCredentialManagingAuthenticationProvider<X>, ScramSaslServerSource
{
public static final String PLAIN = "PLAIN";
private final SecureRandom _random = new SecureRandom();
public static final String QPID_AUTHMANAGER_SCRAM_ITERATION_COUNT = "qpid.auth.scram.iteration_count";
@ManagedContextDefault(name = QPID_AUTHMANAGER_SCRAM_ITERATION_COUNT)
public static final int DEFAULT_ITERATION_COUNT = 4096;
private int _iterationCount = DEFAULT_ITERATION_COUNT;
private boolean _doNotCreateStoredPasswordBecauseItIsBeingUpgraded;
protected AbstractScramAuthenticationManager(final Map<String, Object> attributes, final Broker broker)
{
super(attributes, broker);
}
@StateTransition( currentState = { State.UNINITIALIZED, State.QUIESCED, State.QUIESCED }, desiredState = State.ACTIVE )
protected ListenableFuture<Void> activate()
{
_iterationCount = getContextValue(Integer.class, QPID_AUTHMANAGER_SCRAM_ITERATION_COUNT);
for(ManagedUser user : getUserMap().values())
{
updateStoredPasswordFormatIfNecessary(user);
}
return super.activate();
}
@Override
public List<String> getMechanisms()
{
return Collections.unmodifiableList(Arrays.asList(getMechanismName(), PLAIN));
}
protected abstract String getMechanismName();
@Override
public SaslServer createSaslServer(final String mechanism,
final String localFQDN,
final Principal externalPrincipal)
throws SaslException
{
if(getMechanismName().equals(mechanism))
{
return new ScramSaslServer(this, getMechanismName(), getHmacName(), getDigestName());
}
else if(PLAIN.equals(mechanism))
{
return new PlainAdapterSaslServer(this);
}
else
{
throw new SaslException("Unknown mechanism: " + mechanism);
}
}
protected abstract String getDigestName();
@Override
public AuthenticationResult authenticate(final String username, final String password)
{
ManagedUser user = getUser(username);
if(user != null)
{
updateStoredPasswordFormatIfNecessary(user);
SaltAndPasswordKeys saltAndPasswordKeys = getSaltAndPasswordKeys(username);
try
{
byte[] saltedPassword = createSaltedPassword(saltAndPasswordKeys.getSalt(), password, saltAndPasswordKeys.getIterationCount());
byte[] clientKey = computeHmac(saltedPassword, "Client Key");
byte[] storedKey = MessageDigest.getInstance(getDigestName()).digest(clientKey);
byte[] serverKey = computeHmac(saltedPassword, "Server Key");
if(Arrays.equals(saltAndPasswordKeys.getStoredKey(), storedKey)
&& Arrays.equals(saltAndPasswordKeys.getServerKey(), serverKey))
{
return new AuthenticationResult(new UsernamePrincipal(username, this));
}
}
catch (IllegalArgumentException | NoSuchAlgorithmException | SaslException e)
{
return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR,e);
}
}
return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
}
@Override
public int getIterationCount()
{
return _iterationCount;
}
private static final byte[] INT_1 = new byte[]{0, 0, 0, 1};
private void updateStoredPasswordFormatIfNecessary(final ManagedUser user)
{
final int oldDefaultIterationCount = 4096;
final String[] passwordFields = user.getPassword().split(",");
if (passwordFields.length == 2)
{
byte[] saltedPassword = DatatypeConverter.parseBase64Binary(passwordFields[PasswordField.SALTED_PASSWORD.ordinal()]);
try
{
byte[] clientKey = computeHmac(saltedPassword, "Client Key");
byte[] storedKey = MessageDigest.getInstance(getDigestName()).digest(clientKey);
byte[] serverKey = computeHmac(saltedPassword, "Server Key");
String password = passwordFields[PasswordField.SALT.ordinal()] + ","
+ "," // remove previously insecure salted password field
+ DatatypeConverter.printBase64Binary(storedKey) + ","
+ DatatypeConverter.printBase64Binary(serverKey) + ","
+ oldDefaultIterationCount;
upgradeUserPassword(user, password);
}
catch (NoSuchAlgorithmException e)
{
throw new IllegalArgumentException(e);
}
}
else if (passwordFields.length == 4)
{
String password = passwordFields[PasswordField.SALT.ordinal()] + ","
+ "," // remove previously insecure salted password field
+ passwordFields[PasswordField.STORED_KEY.ordinal()] + ","
+ passwordFields[PasswordField.SERVER_KEY.ordinal()] + ","
+ oldDefaultIterationCount;
upgradeUserPassword(user, password);
}
else if (passwordFields.length != 5)
{
throw new IllegalConfigurationException("password field for user '" + user.getName() + "' has unrecognised format.");
}
}
private void upgradeUserPassword(final ManagedUser user, final String password)
{
try
{
_doNotCreateStoredPasswordBecauseItIsBeingUpgraded = true;
user.setPassword(password);
}
finally
{
_doNotCreateStoredPasswordBecauseItIsBeingUpgraded = false;
}
}
private byte[] createSaltedPassword(byte[] salt, String password, int iterationCount)
{
Mac mac = createShaHmac(password.getBytes(ASCII));
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 byte[] computeHmac(final byte[] key, final String string)
{
Mac mac = createShaHmac(key);
mac.update(string.getBytes(StandardCharsets.US_ASCII));
return mac.doFinal();
}
private Mac createShaHmac(final byte[] keyBytes)
{
try
{
SecretKeySpec key = new SecretKeySpec(keyBytes, getHmacName());
Mac mac = Mac.getInstance(getHmacName());
mac.init(key);
return mac;
}
catch (NoSuchAlgorithmException | InvalidKeyException e)
{
throw new IllegalArgumentException(e.getMessage(), e);
}
}
protected abstract String getHmacName();
@Override
protected String createStoredPassword(final String password)
{
if (_doNotCreateStoredPasswordBecauseItIsBeingUpgraded)
{
return password;
}
try
{
final int iterationCount = getIterationCount();
byte[] salt = generateSalt();
byte[] saltedPassword = createSaltedPassword(salt, password, iterationCount);
byte[] clientKey = computeHmac(saltedPassword, "Client Key");
byte[] storedKey = MessageDigest.getInstance(getDigestName()).digest(clientKey);
byte[] serverKey = computeHmac(saltedPassword, "Server Key");
return DatatypeConverter.printBase64Binary(salt) + ","
+ "," // leave insecure salted password field blank
+ DatatypeConverter.printBase64Binary(storedKey) + ","
+ DatatypeConverter.printBase64Binary(serverKey) + ","
+ iterationCount;
}
catch (NoSuchAlgorithmException e)
{
throw new IllegalArgumentException(e);
}
}
@Override
void validateUser(final ManagedUser managedUser)
{
if(!ASCII.newEncoder().canEncode(managedUser.getName()))
{
throw new IllegalArgumentException("User names are restricted to characters in the ASCII charset");
}
}
@Override
public SaltAndPasswordKeys getSaltAndPasswordKeys(final String username)
{
ManagedUser user = getUser(username);
final byte[] salt;
final byte[] storedKey;
final byte[] serverKey;
final int iterationCount;
final SaslException exception;
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.
salt = generateSalt();
storedKey = null;
serverKey = null;
iterationCount = -1;
exception = new SaslException("Authentication Failed");
}
else
{
updateStoredPasswordFormatIfNecessary(user);
final String[] passwordFields = user.getPassword().split(",");
salt = DatatypeConverter.parseBase64Binary(passwordFields[PasswordField.SALT.ordinal()]);
storedKey = DatatypeConverter.parseBase64Binary(passwordFields[PasswordField.STORED_KEY.ordinal()]);
serverKey = DatatypeConverter.parseBase64Binary(passwordFields[PasswordField.SERVER_KEY.ordinal()]);
iterationCount = Integer.parseInt(passwordFields[PasswordField.ITERATION_COUNT.ordinal()]);
exception = null;
}
return new SaltAndPasswordKeys()
{
@Override
public byte[] getSalt()
{
return salt;
}
@Override
public byte[] getStoredKey() throws SaslException
{
if(storedKey == null)
{
throw exception;
}
return storedKey;
}
@Override
public byte[] getServerKey() throws SaslException
{
if(serverKey == null)
{
throw exception;
}
return serverKey;
}
@Override
public int getIterationCount() throws SaslException
{
if(iterationCount < 0)
{
throw exception;
}
return iterationCount;
}
};
}
private byte[] generateSalt()
{
byte[] tmpSalt = new byte[32];
_random.nextBytes(tmpSalt);
return tmpSalt;
}
private enum PasswordField
{
SALT, SALTED_PASSWORD, STORED_KEY, SERVER_KEY, ITERATION_COUNT
}
}