/* Copyright (c) 2008 Bluendo S.r.L.
* See about.html for details about license.
*
* $Id: SASLAuthenticator.java 1578 2009-06-16 11:07:59Z luca $
*/
package it.yup.xmlstream;
import it.yup.util.GoogleToken;
import it.yup.util.Utils;
import it.yup.xml.Element;
import it.yup.xmpp.Contact;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Hashtable;
import org.bouncycastle.util.encoders.Base64;
// XXX Note if we used a state machine with just one listener we could avoid declaring
// on the fly stubs for calling the correct method. Each stub takes about 900 bytes, which is
// a considerable waste of space
/**
* Class carrying on all the authentications steps
*
*/
public class SASLAuthenticator extends Initializer {
private static String MECHANISM_PLAIN = "PLAIN";
private static String MECHANISM_DIGEST_MD5 = "DIGEST-MD5";
private static String MECHANISM_X_GOOGLE_TOKEN = "X-GOOGLE-TOKEN";
private String supportedMechanisms[] = new String[] { MECHANISM_DIGEST_MD5,
MECHANISM_PLAIN, MECHANISM_X_GOOGLE_TOKEN };
protected SASLAuthenticator() {
// mandatory
super("urn:ietf:params:xml:ns:xmpp-sasl", false);
}
/**
* Start the login process. The result is asynchronous, and in order to get it
* register a listener for the STREAM_CONNECTED event.
*/
public void start(BasicXmlStream xmlStream) {
this.stream = xmlStream;
// Config cfg = xmlStream.config;
// look for the best auth mechanism and start the auth (the first, the better)
Element mechanisms = (Element) stream.features.get(namespace);
Element auth = new Element(namespace, "auth");
for (int i = 0; i < supportedMechanisms.length; i++) {
Element[] children = mechanisms.getChildren();
for (int j = 0; j < children.length; j++) {
Element mechanism = children[j];
if (supportedMechanisms[i].equals(mechanism.getText())) {
if (supportedMechanisms[i].equals(MECHANISM_PLAIN)) {
auth.setAttribute("mechanism", MECHANISM_PLAIN);
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
String tmp = Contact.userhost(stream.jid);
bos.write(Utils.getBytesUtf8(tmp));
bos.write(0);
tmp = Contact.user(stream.jid);
bos.write(Utils.getBytesUtf8(tmp));
bos.write(0);
tmp = stream.password;
bos.write(Utils.getBytesUtf8(tmp));
/* base64 **SHOULD** be ASCII and doesn't need UTF-8 */
auth.addText(new String(Base64.encode(bos
.toByteArray())));
} catch (UnsupportedEncodingException e) {
// YUPMidlet.yup.reportException("UnsupportedEncoding on SASLAutenticator", e, null);
} catch (IOException e) {
// YUPMidlet.yup.reportException("IO error on SASLAutenticator", e, null);
}
// listen for the result
EventQuery pq = new EventQuery(EventQuery.ANY_PACKET,
null, null);
BasicXmlStream.addOnetimeEventListener(pq,
new PacketListener() {
public void packetReceived(Element e) {
if ("success".equals(e.name)) {
stream.restart();
stream
.dispatchEvent(
BasicXmlStream.STREAM_AUTHENTICATED,
null);
} else {
stream
.dispatchEvent(
BasicXmlStream.STREAM_ERROR,
"Cannot authenticate");
}
}
});
stream.send(auth, -1);
// started auth with this method, don't try the others
return;
} else if (supportedMechanisms[i]
.equals(MECHANISM_DIGEST_MD5)) {
auth.setAttribute("mechanism", MECHANISM_DIGEST_MD5);
EventQuery pq = new EventQuery(EventQuery.ANY_PACKET,
null, null);
BasicXmlStream.addOnetimeEventListener(pq,
new PacketListener() {
public void packetReceived(Element e) {
if ("challenge".equals(e.name)) {
gotChallenge(e);
stream
.dispatchEvent(
BasicXmlStream.STREAM_AUTHENTICATED,
null);
} else {
stream
.dispatchEvent(
BasicXmlStream.STREAM_ERROR,
"Cannot authenticate");
}
}
});
stream.send(auth, -1);
return;
} else if (supportedMechanisms[i]
.equals(MECHANISM_X_GOOGLE_TOKEN)) {
auth
.setAttribute("mechanism",
MECHANISM_X_GOOGLE_TOKEN);
String user = Contact.user(stream.jid);
String token = GoogleToken.getToken(user,
stream.password);
try {
String jid = Contact.userhost(stream.jid);
byte jidbytes[] = Utils.getBytesUtf8(jid);
byte tokenbytes[] = Utils.getBytesUtf8(token);
byte buf[] = new byte[2 + jidbytes.length
+ tokenbytes.length];
buf[0] = 0;
System.arraycopy(jidbytes, 0, buf, 1,
jidbytes.length);
buf[jidbytes.length + 1] = 0;
System.arraycopy(tokenbytes, 0, buf,
jidbytes.length + 2, tokenbytes.length);
// #mdebug
//@ System.out.println(new String(Base64.encode(buf)));
// #enddebug
auth.addText(new String(Base64.encode(buf)));
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// listen for the result
EventQuery pq = new EventQuery(EventQuery.ANY_PACKET,
null, null);
BasicXmlStream.addOnetimeEventListener(pq,
new PacketListener() {
public void packetReceived(Element e) {
if ("success".equals(e.name)) {
stream.restart();
stream
.dispatchEvent(
BasicXmlStream.STREAM_AUTHENTICATED,
null);
return;
} else {
stream
.dispatchEvent(
BasicXmlStream.STREAM_ERROR,
"Cannot authenticate");
}
}
});
stream.send(auth, -1);
return;
}
}
}
}
// XXX here we should use a different event
stream.dispatchEvent(BasicXmlStream.STREAM_ERROR,
"Cannot find suitable mechanism for authentication");
}
/**
* Proceed with the challenge reveived from the server (digest md5 auth)
* @param packet
*/
private void gotChallenge(Element packet) {
try {
// decode and parse the challenge
String challengeMessage = new String(Base64
.decode(packet.getText()));
Hashtable challengeDirectives = parse(challengeMessage);
String response_content;
if (challengeDirectives.containsKey("rspauth")) {
response_content = "";
} else {
// generate the response
Hashtable responseDirectives = new Hashtable();
String nonce = (String) challengeDirectives.get("nonce");
responseDirectives.put("nonce", nonce);
String nc = "00000001"; // response sequence number XXX handle subsequents
responseDirectives.put("nc", nc);
// XXX very unsecure, but good for now
String cnonce = Utils.hexDigest(
"" + System.currentTimeMillis(), "md5");
responseDirectives.put("cnonce", cnonce);
String qop = "auth";
responseDirectives.put("qop", qop);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write(Utils.digest(Contact.user(stream.jid) + ":"
+ Contact.domain(stream.jid) + ":" + stream.password,
"md5"));
bos.write(Utils.getBytesUtf8(":" + nonce + ":" + cnonce));
byte A1[] = bos.toByteArray();
String digest_uri = "xmpp/" + Contact.domain(stream.jid); // XXX don't know if this is correct
String A2 = ("AUTHENTICATE:" + digest_uri);
String KD = Utils.bytesToHex(Utils.digest(A1, "md5"))
+ ":"
+ (nonce + ":" + nc + ":" + cnonce + ":" + "auth" + ":" + Utils
.hexDigest(A2, "md5"));
String response = Utils.hexDigest(KD, "md5");
responseDirectives.put("response", response);
responseDirectives.put("charset", "utf-8");
responseDirectives.put("digest-uri", digest_uri);
responseDirectives.put("username", Contact.user(stream.jid));
responseDirectives.put("realm", Contact.domain(stream.jid));
// prepare the response putting together all the directives
response_content = unparse(responseDirectives);
}
Element responseElement = new Element(namespace, "response");
// responseElement.content = new String(Base64.encode(content.getBytes("utf-8")));
// BASE64 **SHOULD** be UTF-8
responseElement.addText(new String(Base64.encode(Utils
.getBytesUtf8(response_content))));
EventQuery pq = new EventQuery(EventQuery.ANY_PACKET, null, null);
BasicXmlStream.addOnetimeEventListener(pq, new PacketListener() {
public void packetReceived(Element e) {
if ("success".equals(e.name)) {
stream.restart();
} else if ("challenge".equals(e.name)) {
gotChallenge(e);
} else if ("failure".equals(e.name)) {
Element child = e
.getChildByName(null, "not-authorized");
if (child != null) stream.dispatchEvent(
BasicXmlStream.NOT_AUTHORIZED,
"Cannot authenticate");
else
stream.dispatchEvent(
BasicXmlStream.REGISTRATION_FAILED,
"Cannot registrate");
} else {
stream.dispatchEvent(BasicXmlStream.STREAM_ERROR,
"Cannot authenticate");
}
}
});
stream.send(responseElement, -1);
} catch (UnsupportedEncodingException e) {
// YUPMidlet.yup.reportException("UnsupportedEncoding on gotChallenge in SASLAutenticator", e, null);
} catch (IOException e1) {
stream.dispatchEvent(BasicXmlStream.STREAM_ERROR,
"Not enough memory for completing the authentication");
}
}
private Hashtable parse(String message) {
Hashtable directives = new Hashtable();
boolean cont = true;
int cur = 0;
while (cont) {
int middle = message.indexOf("=", cur);
String name = message.substring(cur, middle);
middle += 1;
if (message.charAt(middle) == '"') {
middle += 1;
int end = message.indexOf('"', middle);
directives.put(name, message.substring(middle, end));
cur = message.indexOf(",", end) + 1;
if (cur == 0) cont = false;
} else {
int end = message.indexOf(',', middle);
if (end == -1) {
directives.put(name, message.substring(middle).trim());
cont = false;
} else {
directives.put(name, message.substring(middle, end).trim());
}
cur = end + 1;
}
}
return directives;
}
private String unparse(Hashtable directives) {
Enumeration keys = directives.keys();
Hashtable quote = new Hashtable();
StringBuffer out = new StringBuffer();
quote.put("username", "");
quote.put("realm", "");
quote.put("cnonce", "");
quote.put("nonce", "");
quote.put("digest-uri", "");
quote.put("authzid", "");
quote.put("cipher", "");
String join = "";
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String value = (String) directives.get(key);
if (quote.containsKey(key)) {
out.append(join + key + "=" + "\"" + value + "\"");
} else {
out.append(join + key + "=" + value);
}
join = ",";
}
return out.toString();
}
}