summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Smith <git@kismith.co.uk>2011-07-01 09:19:49 (GMT)
committerKevin Smith <git@kismith.co.uk>2011-07-01 09:19:49 (GMT)
commit2da71a8a85486a494343f1662d64fb5ae5a2a44e (patch)
tree23992f9f2a00bac23b345e5c2cc9c1194efc25be /src/com/isode/stroke/sasl
downloadstroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.zip
stroke-2da71a8a85486a494343f1662d64fb5ae5a2a44e.tar.bz2
Initial import
Diffstat (limited to 'src/com/isode/stroke/sasl')
-rw-r--r--src/com/isode/stroke/sasl/ClientAuthenticator.java52
-rw-r--r--src/com/isode/stroke/sasl/PLAINClientAuthenticator.java26
-rw-r--r--src/com/isode/stroke/sasl/SCRAMSHA1ClientAuthenticator.java199
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;
+}