diff options
author | Kevin Smith <git@kismith.co.uk> | 2011-07-01 09:19:49 (GMT) |
---|---|---|
committer | Kevin Smith <git@kismith.co.uk> | 2011-07-01 09:19:49 (GMT) |
commit | 2da71a8a85486a494343f1662d64fb5ae5a2a44e (patch) | |
tree | 23992f9f2a00bac23b345e5c2cc9c1194efc25be /src/com/isode/stroke/sasl | |
download | stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.zip stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.tar.bz2 |
Initial import
Diffstat (limited to 'src/com/isode/stroke/sasl')
3 files changed, 277 insertions, 0 deletions
diff --git a/src/com/isode/stroke/sasl/ClientAuthenticator.java b/src/com/isode/stroke/sasl/ClientAuthenticator.java new file mode 100644 index 0000000..e8fcff9 --- /dev/null +++ b/src/com/isode/stroke/sasl/ClientAuthenticator.java @@ -0,0 +1,52 @@ +/* + * 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; + +public abstract class ClientAuthenticator { + + public ClientAuthenticator(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setCredentials(String authcid, String password) { + setCredentials(authcid, password, ""); + } + + public void setCredentials(String authcid, String password, String authzid) { + this.authcid = authcid; + this.password = password; + this.authzid = authzid; + } + + public abstract ByteArray getResponse(); + + public abstract boolean setChallenge(ByteArray challenge); + + public String getAuthenticationID() { + return authcid; + } + + public String getAuthorizationID() { + return authzid; + } + + public String getPassword() { + return password; + } + private String name; + private String authcid; + private String password; + private String authzid; +} diff --git a/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java b/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java new file mode 100644 index 0000000..889ac54 --- /dev/null +++ b/src/com/isode/stroke/sasl/PLAINClientAuthenticator.java @@ -0,0 +1,26 @@ +/* + * 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; + +public class PLAINClientAuthenticator extends ClientAuthenticator { + public PLAINClientAuthenticator() { + super("PLAIN"); + } + + public ByteArray getResponse() { + return new ByteArray().append(getAuthorizationID()).append((byte)0).append(getAuthenticationID()).append((byte)0).append(getPassword()); + } + + public boolean setChallenge(ByteArray challenge) { + return true; + } +} + diff --git a/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java b/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java new file mode 100644 index 0000000..da44d85 --- /dev/null +++ b/src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java @@ -0,0 +1,199 @@ +/* + * 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; +} |