Add an implementation of the SCRAM-SHA-! SASL mechanism.
This implementation come from the Stroke project http://swift.im/git/stroke
/*
* Copyright (c) 2010, Isode Limited, London, England.
* All rights reserved.
*/
/*
* Copyright (c) 2010, Remko Tron�on.
* All rights reserved.
*/
package com.isode.stroke.sasl;
import com.isode.stroke.base.ByteArray;
import com.isode.stroke.stringcodecs.Base64;
import com.isode.stroke.stringcodecs.HMACSHA1;
import com.isode.stroke.stringcodecs.PBKDF2;
import com.isode.stroke.stringcodecs.SHA1;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.HashMap;
import java.util.Map;
public class SCRAMSHA1ClientAuthenticator extends ClientAuthenticator {
static String escape(String s) {
String result = "";
for (int i = 0; i < s.length(); ++i) {
if (s.charAt(i) == ',') {
result += "=2C";
} else if (s.charAt(i) == '=') {
result += "=3D";
} else {
result += s.charAt(i);
}
}
return result;
}
public SCRAMSHA1ClientAuthenticator(String nonce) {
this(nonce, false);
}
public SCRAMSHA1ClientAuthenticator(String nonce, boolean useChannelBinding) {
super(useChannelBinding ? "SCRAM-SHA-1-PLUS" : "SCRAM-SHA-1");
step = Step.Initial;
clientnonce = nonce;
this.useChannelBinding = useChannelBinding;
}
public void setTLSChannelBindingData(ByteArray channelBindingData) {
tlsChannelBindingData = channelBindingData;
}
public ByteArray getResponse() {
if (step.equals(Step.Initial)) {
return ByteArray.plus(getGS2Header(), getInitialBareClientMessage());
} else if (step.equals(Step.Proof)) {
ByteArray clientKey = HMACSHA1.getResult(saltedPassword, new ByteArray("Client Key"));
ByteArray storedKey = SHA1.getHash(clientKey);
ByteArray clientSignature = HMACSHA1.getResult(storedKey, authMessage);
ByteArray clientProof = clientKey;
byte[] clientProofData = clientProof.getData();
for (int i = 0; i < clientProofData.length; ++i) {
clientProofData[i] ^= clientSignature.getData()[i];
}
ByteArray result = getFinalMessageWithoutProof().append(",p=").append(Base64.encode(clientProof));
return result;
} else {
return null;
}
}
public boolean setChallenge(ByteArray challenge) {
if (step.equals(Step.Initial)) {
if (challenge == null) {
return false;
}
initialServerMessage = challenge;
Map<Character, String> keys = parseMap(initialServerMessage.toString());
// Extract the salt
ByteArray salt = Base64.decode(keys.get('s'));
// Extract the server nonce
String clientServerNonce = keys.get('r');
if (clientServerNonce.length() <= clientnonce.length()) {
return false;
}
String receivedClientNonce = clientServerNonce.substring(0, clientnonce.length());
if (!receivedClientNonce.equals(clientnonce)) {
return false;
}
serverNonce = new ByteArray(clientServerNonce.substring(clientnonce.length()));
// Extract the number of iterations
int iterations = 0;
try {
iterations = Integer.parseInt(keys.get('i'));
} catch (NumberFormatException e) {
return false;
}
if (iterations <= 0) {
return false;
}
ByteArray channelBindData = new ByteArray();
if (useChannelBinding && tlsChannelBindingData != null) {
channelBindData = tlsChannelBindingData;
}
// Compute all the values needed for the server signature
saltedPassword = PBKDF2.encode(new ByteArray(SASLPrep(getPassword())), salt, iterations);
authMessage = getInitialBareClientMessage().append(",").append(initialServerMessage).append(",").append(getFinalMessageWithoutProof());
ByteArray serverKey = HMACSHA1.getResult(saltedPassword, new ByteArray("Server Key"));
serverSignature = HMACSHA1.getResult(serverKey, authMessage);
step = Step.Proof;
return true;
} else if (step.equals(step.Proof)) {
ByteArray result = new ByteArray("v=").append(new ByteArray(Base64.encode(serverSignature)));
step = Step.Final;
return challenge != null && challenge.equals(result);
} else {
return true;
}
}
private String SASLPrep(String source) {
return Normalizer.normalize(source, Form.NFKC); /* FIXME: Implement real SASLPrep */
}
private Map<Character, String> parseMap(String s) {
HashMap<Character, String> result = new HashMap<Character, String>();
if (s.length() > 0) {
char key = '~'; /* initialise so it'll compile */
String value = "";
int i = 0;
boolean expectKey = true;
while (i < s.length()) {
if (expectKey) {
key = s.charAt(i);
expectKey = false;
i++;
} else if (s.charAt(i) == ',') {
result.put(key, value);
value = "";
expectKey = true;
} else {
value += s.charAt(i);
}
i++;
}
result.put(key, value);
}
return result;
}
private ByteArray getInitialBareClientMessage() {
String authenticationID = SASLPrep(getAuthenticationID());
return new ByteArray("n=" + escape(authenticationID) + ",r=" + clientnonce);
}
private ByteArray getGS2Header() {
ByteArray channelBindingHeader = new ByteArray("n");
if (tlsChannelBindingData != null) {
if (useChannelBinding) {
channelBindingHeader = new ByteArray("p=tls-unique");
}
else {
channelBindingHeader = new ByteArray("y");
}
}
return new ByteArray().append(channelBindingHeader).append(",").append(getAuthorizationID().isEmpty() ? new ByteArray() : new ByteArray("a=" + escape(getAuthorizationID()))).append(",");
}
private ByteArray getFinalMessageWithoutProof() {
ByteArray channelBindData = new ByteArray();
if (useChannelBinding && tlsChannelBindingData != null) {
channelBindData = tlsChannelBindingData;
}
return new ByteArray("c=" + Base64.encode(new ByteArray(getGS2Header()).append(channelBindData)) + ",r=" + clientnonce).append(serverNonce);
}
private enum Step {
Initial,
Proof,
Final
};
private Step step;
private String clientnonce = "";
private ByteArray initialServerMessage = new ByteArray();
private ByteArray serverNonce = new ByteArray();
private ByteArray authMessage = new ByteArray();
private ByteArray saltedPassword = new ByteArray();
private ByteArray serverSignature = new ByteArray();
private boolean useChannelBinding;
private ByteArray tlsChannelBindingData;
}