package edu.washington.cs.publickey.xmpp.client;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.Base64;
import edu.washington.cs.publickey.CryptoHandler;
import edu.washington.cs.publickey.PublicKeyClient;
import edu.washington.cs.publickey.PublicKeyFriend;
import edu.washington.cs.publickey.Tools;
import edu.washington.cs.publickey.xmpp.XMPPNetwork;
public class PublicKeyXmppClient extends PublicKeyClient {
private static final long TIMEOUT = 20 * 1000;
private boolean disconnectRequested = false;
private final XMPPConnection connection;
private final PublicKeyFriend me;
private final XMPPNetwork network;
private List<PublicKeyFriend> networkFriends;
private final char[] password;
private String serverUserId;
private final CryptoHandler signer;
private String status = "";
private final String username;
public PublicKeyXmppClient(File knownFriendsFile, List<byte[]> knownKeys, XMPPNetwork network, String username, char[] password, String localKeyNick, CryptoHandler signer) throws Exception {
super(knownFriendsFile, knownKeys);
this.signer = signer;
this.password = password;
this.network = network;
this.username = username;
log("Starting IM client");
ConnectionConfiguration connConfig = new ConnectionConfiguration(network.getServerAddr(), network.getServerPort(), network.getServiceName());
connection = new XMPPConnection(connConfig);
// create the "me" user
me = new PublicKeyFriend();
me.setKeyNick(localKeyNick);
me.setPublicKey(signer.getPublicKey().getEncoded());
me.setPublicKeySha1(Tools.getSha1(signer.getPublicKey().getEncoded()));
me.setSourceNetwork(network.getFriendNetwork());
me.setSourceNetworkUid(Tools.getSha1(username));
me.setRealName("Me");
}
/**
* Connect to the xmpp server
*/
public void connect() throws Exception {
connection.connect();
status = "connected";
log(status);
// TODO: talk to the smack guys about using a char[] for passwords so
// it can get overwritten
connection.login(username, new String(password));
status = "logged in to: " + network.getDisplayName();
log(status);
// set presence
Presence p = new Presence(Presence.Type.unavailable);
connection.sendPacket(p);
// don't allow any new users on my watch
connection.getRoster().setSubscriptionMode(Roster.SubscriptionMode.reject_all);
// add the publickey serverbot to the contact list
// so we can send and receive messages from it
// if (!connection.getRoster().contains(getServerBotUserId())) {
// connection.getRoster().createEntry(getServerBotUserId(),
// serverBotName, null);
// }
}
private String serverBotName = "PublicKey Server Bot";
public void setServerBotName(String name) {
this.serverBotName = name;
}
/**
* Create a client hello message
*
*The message contains the public key of the user
*
* @param myKey
* @return
* @throws IOException
*/
private Message createClientHello(PublicKeyFriend me) throws IOException {
Message msg = new Message(getServerBotUserId(), Message.Type.chat);
msg.setBody(null);
// we only have to set the public key and key nick fienlds here, the
// rest is calculated
// on the server anyway (it won't trust any of that data from here)
PublicKeyFriend myKey = new PublicKeyFriend();
myKey.setPublicKey(me.getPublicKey());
myKey.setKeyNick(me.getKeyNick());
msg.setProperty(Tools.PUBLICKEY_PAYLOAD_KEY__PublicKeyFriend, myKey.serialize());
msg.setFrom(connection.getUser());
return msg;
}
/**
* Creates a client request packet based on the server challenge packet
*
* @param serverChallenge
* @return
* @throws Exception
*/
private Message createClientRequest(Packet serverChallenge) throws Exception {
Object nounceObj = serverChallenge.getProperty(Tools.PUBLICKEY_PAYLOAD_NOUNCE__base64_byte_array);
if (nounceObj != null && nounceObj instanceof String) {
byte[] nounce = Base64.decode((String) nounceObj);
byte[] signature = signer.sign(nounce);
Message m = new Message();
m.setPacketID(serverChallenge.getPacketID());
m.setTo(serverChallenge.getFrom());
m.setBody(null);
m.setFrom(connection.getUser());
m.setProperty(Tools.PUBLICKEY_PAYLOAD_SIGNATURE__String_Base64, Base64.encodeBytes(signature, Base64.DONT_BREAK_LINES));
m.setProperty(Tools.PUBLICKEY_PAYLOAD_FRIENDS__MergeSha1_Base64, getFriendsCompact());
List<byte[]> knownKeyList = getKnownKeySha1s();
m.setProperty(Tools.PUBLICKEY_PAYLOAD_KNOWN_KEYS__MergeSha1_Base64, Tools.mergeSha1sAndBase64(knownKeyList));
return m;
} else {
throw new Exception("server handshake invalid, expected random nounce, got: '" + nounceObj + "'");
}
}
public void disconnect() throws Exception {
if (connection.isConnected() && !disconnectRequested) {
disconnectRequested = true;
/*
* do this in a separate thread
*/
Thread t = new Thread(new Runnable() {
public void run() {
try {
// clean up the connection, unsubscribe the bot and set
// to
// unavailable
log("sending presence unsubscribe to: " + serverUserId);
status = "cleaning up connection";
Presence presenceUnsubscribe = new Presence(Presence.Type.unsubscribe);
presenceUnsubscribe.setTo(serverUserId);
connection.sendPacket(presenceUnsubscribe);
RosterEntry entry = connection.getRoster().getEntry(getServerBotUserId());
if (entry != null) {
connection.getRoster().removeEntry(entry);
System.out.println("removing: " + entry.getUser() + " from roster");
}
Presence presenceUnavailable = new Presence(Presence.Type.unavailable);
connection.sendPacket(presenceUnavailable);
log(status);
} catch (XMPPException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
connection.disconnect();
} catch (java.security.AccessControlException e) {
System.err.println("unable to close connection (check if thread has modifyThread permission)");
}
}
});
t.setDaemon(true);
t.setName("XMPP disconnect thread");
t.start();
} else {
status = "disconnected";
}
for (int i = 0; i < password.length; i++) {
password[i] = 'a';
}
}
private String getFriendsCompact() throws NoSuchAlgorithmException, IOException {
networkFriends = new LinkedList<PublicKeyFriend>();
// add self both for mapping and to make the server not return us
networkFriends.add(me);
byte[] serverSha1 = Tools.getSha1(getServerBotUserId());
List<byte[]> friends = new LinkedList<byte[]>();
for (RosterEntry friend : connection.getRoster().getEntries()) {
byte[] sha1 = Tools.getSha1(friend.getUser());
// dont add the server bot
if (!Arrays.equals(serverSha1, sha1)) {
friends.add(sha1);
PublicKeyFriend f = new PublicKeyFriend();
f.setSourceNetwork(network.getFriendNetwork());
f.setSourceNetworkUid(sha1);
String name = friend.getName();
if (name != null) {
f.setRealName(name);
} else {
f.setRealName(friend.getUser());
}
networkFriends.add(f);
}
}
super.addKnownFriends(networkFriends);
return Tools.mergeSha1sAndBase64(friends);
}
/**
* This is the meat of the xmpp client: it follows the
* "publickey xmpp protocol"
*
* 1: Client->Server: clients public key
*
* 2: S->C: nounce to sign
*
* 3: C->S: signature of nounce, list of friends, list of sha1 of known keys
*
* 4: S->C: list of new friends
*/
public void updateFriends() throws Exception {
try {
// 0.1: send the presence packet to the bot
try {
log("sending presence subscribe to: " + serverUserId + ", waiting");
status = "locating friend finder";
Presence presenceSubscribe = new Presence(Presence.Type.subscribe);
presenceSubscribe.setTo(serverUserId);
sendPacketAndWaitForResponse(presenceSubscribe);
log("got presence subscribed: " + serverUserId);
} catch (XMPPException e) {
log("got no presence subscribed response, trying to continue anyway");
}
// 0.2 set presence to available
try {
status = "sending status=online";
Presence presenceAvailable = new Presence(Presence.Type.available);
presenceAvailable.setMode(Presence.Mode.xa);
presenceAvailable.setTo(serverUserId);
sendPacketAndWaitForResponse(presenceAvailable);
log(status);
} catch (XMPPException e) {
log("got no presence available response, trying to continue anyway");
}
// 1: Client->Server: clients public key
Message msg = createClientHello(me);
status = "sending client hello, waiting for server";
log(status);
// 2: S->C: nounce to sign
Packet serverChallenge = sendPacketAndWaitForResponse(msg);
status = "got server challenge, signing + sending client request";
log(status);
Message clientRequest = createClientRequest(serverChallenge);
// 3: C->S: signature of nounce, list of friends, list of sha1 of
// known keys
Packet serverResponse = sendPacketAndWaitForResponse(clientRequest);
status = "sent client request, waiting for friend list";
log(status);
// 4: S->C: list of new friends
Object resp = serverResponse.getProperty(Tools.PUBLICKEY_PAYLOAD_FRIENDS_KEYS__PublicKeyFriend_array);
if (resp instanceof String) {
PublicKeyFriend[] friends = PublicKeyFriend.deserialize((String) resp);
status = "got server response, new friend count: " + friends.length;
addKnownFriends(Arrays.asList(friends));
disconnect();
} else {
disconnect();
throw new Exception("strange, got non friend array back");
}
} catch (Exception e) {
disconnect();
throw e;
}
}
public String getStatus() {
return status;
}
public String getServerBotUserId() {
String toUser;
if (serverUserId == null) {
toUser = Tools.DEFAULT_PUBLIC_KEY_SERVER;
} else {
toUser = serverUserId;
}
return toUser;
}
private void log(String mesg) {
System.out.println(username + ": " + mesg);
}
private Packet sendPacketAndWaitForResponse(Packet packet) throws XMPPException {
log("sending packet: " + packet.toXML());
PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packet.getPacketID()));
connection.sendPacket(packet);
log("waiting for response");
Packet response = collector.nextResult(TIMEOUT);
collector.cancel();
if (response == null) {
throw new XMPPException("No response from the server.");
} else if (response.getError() != null) {
XMPPError error = response.getError();
throw new XMPPException("Got error from xmpp server '" + network.getServerAddr() + ":" + network.getServerPort() + "/" + serverUserId + "' error:" + error);
}
log("got response: " + response.toXML());
return response;
}
public void setServerBotUserId(String serverUserId) throws Exception {
if (!connection.isConnected()) {
this.serverUserId = serverUserId;
} else {
throw new Exception("setting server user id is " + "not allowed after connect()");
}
}
}