blob: ac7d9ef68286d145cd3becb87ae36c5b1ce99dac [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.federation.sasl;
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.sasl.SaslClient;
import javax.security.sasl.SaslException;
import javax.xml.bind.DatatypeConverter;
import org.apache.qpid.server.model.UsernamePasswordCredential;
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 UsernamePasswordCredential<?> _credentials;
private State _state = State.INITIAL;
public AbstractScramSaslClient(final UsernamePasswordCredential credentials,
final String mechanism,
final String digestName,
final String hmacName)
{
_mechanism = mechanism;
_digestName = digestName;
_hmacName = hmacName;
_credentials = credentials;
}
@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 = new byte[0];
_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");
}
byte[] passwordBytes = saslPrep(_credentials.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 (IllegalArgumentException | NoSuchAlgorithmException | IOException 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=");
_username = _credentials.getUsername();
buf.append(saslPrep(_username));
buf.append(",r=");
buf.append(_clientNonce);
_clientFirstMessageBare = buf.toString();
return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII);
}
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
{
}
}