Implement an ScramSaslMechanism to use it with Smack.
authorDa Risk <da_risk@beem-project.com>
Sun, 13 Jan 2013 20:36:11 +0100
changeset 1016 c337d8c387f5
parent 1015 63669480c941
child 1017 377cc9bcfdaa
Implement an ScramSaslMechanism to use it with Smack. Fix #476
src/com/beem/project/beem/BeemService.java
src/com/beem/project/beem/smack/sasl/ScramSaslClient.java
src/com/beem/project/beem/smack/sasl/ScramSaslMechanism.java
--- a/src/com/beem/project/beem/BeemService.java	Sun Jan 13 20:11:53 2013 +0100
+++ b/src/com/beem/project/beem/BeemService.java	Sun Jan 13 20:36:11 2013 +0100
@@ -77,6 +77,7 @@
 import com.beem.project.beem.smack.avatar.AvatarProvider;
 import com.beem.project.beem.smack.ping.PingExtension;
 import com.beem.project.beem.smack.sasl.SASLGoogleOAuth2Mechanism;
+import com.beem.project.beem.smack.sasl.ScramSaslMechanism;
 import com.beem.project.beem.utils.BeemBroadcastReceiver;
 import com.beem.project.beem.utils.BeemConnectivity;
 import com.beem.project.beem.utils.Status;
@@ -580,7 +581,10 @@
 	/* register additionnals sasl mechanisms */
 	SASLAuthentication.registerSASLMechanism(SASLGoogleOAuth2Mechanism.MECHANISM_NAME,
 	    SASLGoogleOAuth2Mechanism.class);
+	SASLAuthentication.registerSASLMechanism(ScramSaslMechanism.MECHANISM_NAME,
+	    ScramSaslMechanism.class);
 
+	SASLAuthentication.supportSASLMechanism(ScramSaslMechanism.MECHANISM_NAME);
 	// Configure entity caps manager. This must be done only once
 	File f = new File(getCacheDir(), "entityCaps");
 	f.mkdirs();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/beem/project/beem/smack/sasl/ScramSaslClient.java	Sun Jan 13 20:36:11 2013 +0100
@@ -0,0 +1,339 @@
+/*
+    BEEM is a videoconference application on the Android Platform.
+
+    Copyright (C) 2009-2012 by Frederic-Charles Barthelery,
+                               Nikita Kozlov,
+                               Vincent Veronis.
+
+    This file is part of BEEM.
+
+    BEEM is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    BEEM is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with BEEM.  If not, see <http://www.gnu.org/licenses/>.
+
+    Please send bug reports with examples or suggestions to
+    contact@beem-project.com or http://www.beem-project.com/
+
+*/
+package com.beem.project.beem.smack.sasl;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import com.isode.stroke.base.ByteArray;
+import com.isode.stroke.sasl.SCRAMSHA1ClientAuthenticator;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+
+/**
+ * A SaslClient which uses the SCRAM-SHA-! mechanism.
+ * This implementation is based on Stroke (http://swift.im/git/stroke)
+ */
+public class ScramSaslClient implements SaslClient {
+
+    private static final int   NONCE_BYTE_COUNT = 32;
+    private static final int   NONCE_HEX_COUNT = 2 * NONCE_BYTE_COUNT;
+    private SCRAMSHA1ClientAuthenticator clientAuthenticator;
+    private CallbackHandler cbh;
+    private String authzid;
+
+    /**
+     * Create a ScramSaslClient.
+     * @param authorizationId the authorizationId uses by the client
+     * @param cbh a CallbackHandler to get more informations
+     */
+    public ScramSaslClient(final String authorizationId, final CallbackHandler cbh) {
+	this.cbh = cbh;
+	this.authzid = authorizationId;
+    }
+
+    /**
+     * Disposes of any system resources or security-sensitive information the
+     * SaslClient might be using. Invoking this method invalidates the
+     * SaslClient instance. This method is idempotent.
+     *
+     * @exception SaslException  if a problem was encountered while disposing
+     *                           of the resources
+     */
+    @Override
+    public void dispose() throws SaslException {
+    }
+
+    /**
+     * Evaluates the challenge data and generates a response. If a challenge
+     * is received from the server during the authentication process, this
+     * method is called to prepare an appropriate next response to submit to
+     * the server.
+     *
+     * @param challenge  The non-null challenge sent from the server. The
+     *                   challenge array may have zero length.
+     *
+     * @return    The possibly null reponse to send to the server. It is null
+     *            if the challenge accompanied a "SUCCESS" status and the
+     *            challenge only contains data for the client to update its
+     *            state and no response needs to be sent to the server.
+     *            The response is a zero-length byte array if the client is to
+     *            send a response with no data.
+     *
+     * @exception SaslException   If an error occurred while processing the
+     *                            challenge or generating a response.
+     */
+    @Override
+    public byte[] evaluateChallenge(byte[] challenge) throws SaslException {
+	if (clientAuthenticator == null) {
+	    Object[] userInfo = getUserInfo();
+	    String authcid = (String) userInfo[0];
+	    byte[] passwdBytes = (byte[]) userInfo[1];
+	    String passwd = new String(passwdBytes);
+	    String nonce = getClientNonce();
+	    clientAuthenticator = new SCRAMSHA1ClientAuthenticator(nonce);
+	    clientAuthenticator.setCredentials(authcid, passwd, authzid);
+	    return clientAuthenticator.getResponse().getData();
+	}
+	clientAuthenticator.setChallenge(new ByteArray(challenge));
+	return clientAuthenticator.getResponse().getData();
+    }
+
+    /**
+     * Returns the IANA-registered mechanism name of this SASL client.
+     *  (e.g. "CRAM-MD5", "GSSAPI")
+     *
+     * @return  "SCRAM-SHA-!" the IANA-registered mechanism name of this SASL
+     *          client.
+     */
+    @Override
+    public String getMechanismName() {
+	return "SCRAM-SHA-1";
+    }
+
+    /**
+     * Retrieves the negotiated property. This method can be called only after
+     * the authentication exchange has completed (i.e., when isComplete()
+     * returns true); otherwise, an IllegalStateException is thrown.
+     *
+     *
+     * @param propName   The non-null property name
+     *
+     * @return  The value of the negotiated property. If null, the property was
+     *          not negotiated or is not applicable to this mechanism. This
+     *          implementation allways returns null.
+     *
+     * @exception IllegalStateException   if this authentication exchange has
+     *                                    not completed
+     */
+    @Override
+    public Object getNegotiatedProperty(String propName) {
+	return null;
+    }
+
+    /**
+     * Determines if this mechanism has an optional initial response. If true,
+     * caller should call evaluateChallenge() with an empty array to get the
+     * initial response.
+     *
+     * @return  true if this mechanism has an initial response. This
+     * 		implementation always return true.
+     */
+    @Override
+    public boolean hasInitialResponse() {
+	return true;
+    }
+
+    /**
+     * Determines if the authentication exchange has completed. This method
+     * may be called at any time, but typically, it will not be called until
+     * the caller has received indication from the server (in a protocol-
+     * specific manner) that the exchange has completed.
+     *
+     * @return  true if the authentication exchange has completed;
+     *           false otherwise.
+     */
+    @Override
+    public boolean isComplete() {
+	if (clientAuthenticator == null)
+	    return false;
+	return clientAuthenticator.getResponse() == null;
+    }
+
+    /**
+     * Unwraps a byte array received from the server. This method can be called
+     * only after the authentication exchange has completed (i.e., when
+     * isComplete() returns true) and only if the authentication exchange has
+     * negotiated integrity and/or privacy as the quality of protection;
+     * otherwise, an IllegalStateException is thrown.
+     *
+     * This implementation always throw an IllegalStateException.
+     *
+     * incoming is the contents of the SASL buffer as defined in RFC 2222
+     * without the leading four octet field that represents the length.
+     * offset and len specify the portion of incoming to use.
+     *
+     * @param incoming   A non-null byte array containing the encoded bytes
+     *                   from the server
+     * @param offset     The starting position at incoming of the bytes to use
+     *
+     * @param len        The number of bytes from incoming to use
+     *
+     * @return           A non-null byte array containing the decoded bytes
+     * @throws SaslException if an error occurs
+     *
+     */
+    @Override
+    public byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException {
+	throw new IllegalStateException("SCRAM-SHA-1: this mechanism supports "
+		+ "neither integrity nor privacy");
+    }
+
+    /**
+     * Wraps a byte array to be sent to the server. This method can be called
+     * only after the authentication exchange has completed (i.e., when
+     * isComplete() returns true) and only if the authentication exchange has
+     * negotiated integrity and/or privacy as the quality of protection;
+     * otherwise, an IllegalStateException is thrown.
+     *
+     * This implementation always throw an IllegalStateException.
+     *
+     * The result of this method will make up the contents of the SASL buffer as
+     * defined in RFC 2222 without the leading four octet field that represents
+     * the length. offset and len specify the portion of outgoing to use.
+     *
+     * @param outgoing   A non-null byte array containing the bytes to encode
+     * @param offset     The starting position at outgoing of the bytes to use
+     * @param len        The number of bytes from outgoing to use
+     *
+     * @return A non-null byte array containing the encoded bytes
+     *
+     * @exception SaslException  if incoming cannot be successfully unwrapped.
+     *
+     * @exception IllegalStateException   if the authentication exchange has
+     *                   not completed, or if the negotiated quality of
+     *                   protection has neither integrity nor privacy.
+     */
+    @Override
+    public byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException {
+	throw new IllegalStateException("SCRAM-SHA-1: this mechanism supports "
+		+ "neither integrity nor privacy");
+    }
+
+
+    /**
+     * Calculates the Nonce value of the Client.
+     *
+     * @return   Nonce value of the client
+     *
+     * @exception   SaslException If an error Occurs
+     */
+    private String getClientNonce() throws SaslException {
+        byte[]          nonceBytes = new byte[NONCE_BYTE_COUNT];
+        SecureRandom    prng;
+        byte            nonceByte;
+        char[]          hexNonce = new char[NONCE_HEX_COUNT];
+
+        try {
+            prng = SecureRandom.getInstance("SHA1PRNG");
+            prng.nextBytes(nonceBytes);
+            for (int i = 0; i < NONCE_BYTE_COUNT; i++) {
+                //low nibble
+                hexNonce[i * 2] = getHexChar((byte) (nonceBytes[i] & 0x0f));
+                //high nibble
+                hexNonce[(i * 2) + 1] = getHexChar((byte) ((nonceBytes[i] & 0xf0) >> 4));
+            }
+            return new String(hexNonce);
+        } catch (NoSuchAlgorithmException e) {
+            throw new SaslException("No random number generator available", e);
+        }
+    }
+
+    /**
+     * Get informations supplied by the user of the SaslClient.
+     * These informations are retrived by using the CallbackHandler.
+     *
+     * @return an array of object
+     * @throws SaslException if the informations cannot be retrieved
+     */
+    private Object[] getUserInfo() throws SaslException {
+        try {
+            final String userPrompt = "Authentication id: ";
+            final String pwPrompt = "Password: ";
+            NameCallback nameCb = new NameCallback(userPrompt);
+            PasswordCallback passwordCb = new PasswordCallback(pwPrompt, false);
+            cbh.handle(new Callback[] {nameCb, passwordCb });
+            String userid = nameCb.getName();
+            char[] pwchars = passwordCb.getPassword();
+            byte[] pwbytes;
+            if (pwchars != null) {
+                pwbytes = (new String(pwchars)).getBytes("UTF8");
+                passwordCb.clearPassword();
+            } else {
+                pwbytes = null;
+            }
+            return new Object[] {userid, pwbytes };
+        } catch (IOException e) {
+            throw new SaslException("Cannot get password", e);
+        } catch (UnsupportedCallbackException e) {
+            throw new SaslException("Cannot get userid/password", e);
+        }
+    }
+
+    /**
+     * This function returns hex character representing the value of the input.
+     *
+     * @param value Input value in byte
+     *
+     * @return Hex value of the Input byte value
+     */
+    private static char getHexChar(byte value) {
+	switch (value) {
+	    case 0:
+		return '0';
+	    case 1:
+		return '1';
+	    case 2:
+		return '2';
+	    case 3:
+		return '3';
+	    case 4:
+		return '4';
+	    case 5:
+		return '5';
+	    case 6:
+		return '6';
+	    case 7:
+		return '7';
+	    case 8:
+		return '8';
+	    case 9:
+		return '9';
+	    case 10:
+		return 'a';
+	    case 11:
+		return 'b';
+	    case 12:
+		return 'c';
+	    case 13:
+		return 'd';
+	    case 14:
+		return 'e';
+	    case 15:
+		return 'f';
+	    default:
+		return 'Z';
+	}
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/beem/project/beem/smack/sasl/ScramSaslMechanism.java	Sun Jan 13 20:36:11 2013 +0100
@@ -0,0 +1,81 @@
+/*
+    BEEM is a videoconference application on the Android Platform.
+
+    Copyright (C) 2009-2012 by Frederic-Charles Barthelery,
+                               Nikita Kozlov,
+                               Vincent Veronis.
+
+    This file is part of BEEM.
+
+    BEEM is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    BEEM is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with BEEM.  If not, see <http://www.gnu.org/licenses/>.
+
+    Please send bug reports with examples or suggestions to
+    contact@beem-project.com or http://www.beem-project.com/
+
+*/
+package com.beem.project.beem.smack.sasl;
+
+import java.io.IOException;
+
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.jivesoftware.smack.SASLAuthentication;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+
+/**
+ * An implementation of the SCRAM-SHA-1 SASL mechanism.
+ * This implementation is based on Stroke http://swift.im/git/stroke.
+ */
+public class ScramSaslMechanism extends SASLMechanism {
+
+    /**
+     * The name of the SASL mechanism.
+     */
+    public static final String MECHANISM_NAME = "SCRAM-SHA-1";
+
+    /**
+     * Create a ScramSaslMechanism.
+     *
+     * @param saslAuthentication the smack SASLAuthentication.
+     *
+     */
+    public ScramSaslMechanism(final SASLAuthentication saslAuthentication) {
+	super(saslAuthentication);
+    }
+
+    @Override
+    public void authenticate(String username, String host, String password) throws IOException, XMPPException {
+	this.authenticationId = username;
+	this.password = password;
+	this.hostname = host;
+
+	sc = new ScramSaslClient(username, this);
+	authenticate();
+    }
+
+    @Override
+    public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {
+	this.authenticationId = username;
+	this.hostname = host;
+	sc = new ScramSaslClient(username, cbh);
+	authenticate();
+    }
+
+    @Override
+    protected String getName() {
+	return MECHANISM_NAME;
+    }
+
+}