merge
authorNikita Kozlov <nikita@mbdsys.com>
Sun, 05 Dec 2010 18:45:54 +0100
changeset 812 9d3a7af41ec2
parent 811 6cbb9b3117b7 (current diff)
parent 810 0ff0059f2ec3 (diff)
child 814 2ef1c6096069
merge
--- a/default.properties	Sun Dec 05 18:45:47 2010 +0100
+++ b/default.properties	Sun Dec 05 18:45:54 2010 +0100
@@ -7,5 +7,7 @@
 # "build.properties", and override values to adapt the script to your
 # project structure.
 
+# Indicates whether an apk should be generated for each density.
+split.density=false
 # Project target.
-target=android-7
+target=android-8
Binary file libs/junit-4.6.jar has changed
Binary file libs/lcrypto-jdk16-143.jar has changed
--- a/res/layout/chat.xml	Sun Dec 05 18:45:47 2010 +0100
+++ b/res/layout/chat.xml	Sun Dec 05 18:45:54 2010 +0100
@@ -27,6 +27,10 @@
 				android:layout_width="fill_parent" android:layout_height="wrap_content"
 				android:textStyle="italic" android:textSize="12sp"
 				android:focusable="true"/>
+			<TextView android:id="@+id/chat_contact_otr_state"
+				android:layout_width="fill_parent" android:layout_height="wrap_content"
+				android:textStyle="italic" android:textSize="12sp"
+				android:focusable="true"/>
 		</LinearLayout>
 	</LinearLayout>
 	<View android:layout_width="fill_parent" android:layout_height="2dp"
--- a/res/menu/chat.xml	Sun Dec 05 18:45:47 2010 +0100
+++ b/res/menu/chat.xml	Sun Dec 05 18:45:54 2010 +0100
@@ -7,4 +7,8 @@
 	</group>
 	<item android:id="@+id/chat_menu_close_chat" android:visible="true"
 		android:title="@string/chat_menu_close_chat" android:icon="@drawable/ic_menu_end_conversation" />
+	<item android:id="@+id/chat_menu_start_otr_session" android:visible="true"
+		android:title="@string/chat_menu_start_otr_session" />
+	<item android:id="@+id/chat_menu_stop_otr_session" android:visible="true"
+		android:title="@string/chat_menu_stop_otr_session" />
 </menu>
--- a/res/values/strings.xml	Sun Dec 05 18:45:47 2010 +0100
+++ b/res/values/strings.xml	Sun Dec 05 18:45:54 2010 +0100
@@ -249,6 +249,8 @@
 	<string name="chat_send_message">Send</string>
 	<string name="chat_menu_contacts_list">Contacts list</string>
 	<string name="chat_menu_change_chat">Switch chat</string>
+	<string name="chat_menu_start_otr_session">Start OTR session</string>
+	<string name="chat_menu_stop_otr_session">Stop OTR session</string>
 	<string name="chat_dialog_change_chat_title">Opened chats</string>
 	<string name="chat_menu_close_chat">Close this chat</string>
 	<string name="chat_no_more_chats">No more active chats</string>
--- a/src/com/beem/project/beem/service/BeemChatManager.java	Sun Dec 05 18:45:47 2010 +0100
+++ b/src/com/beem/project/beem/service/BeemChatManager.java	Sun Dec 05 18:45:54 2010 +0100
@@ -306,5 +306,9 @@
 
 	@Override
 	public void stateChanged(final IChat chat) { }
+
+	@Override
+	public void otrStateChanged(String otrState) throws RemoteException {
+	}
     }
 }
--- a/src/com/beem/project/beem/service/ChatAdapter.java	Sun Dec 05 18:45:47 2010 +0100
+++ b/src/com/beem/project/beem/service/ChatAdapter.java	Sun Dec 05 18:45:54 2010 +0100
@@ -49,8 +49,8 @@
 
 import org.jivesoftware.smack.Chat;
 import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.ChatState;
 import org.jivesoftware.smackx.ChatStateListener;
-import org.jivesoftware.smackx.ChatState;
 
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -58,6 +58,7 @@
 
 import com.beem.project.beem.service.aidl.IChat;
 import com.beem.project.beem.service.aidl.IMessageListener;
+import com.zadov.beem.BeemOtrManager;
 
 /**
  * An adapter for smack's Chat class.
@@ -74,6 +75,7 @@
     private final List<Message> mMessages;
     private final RemoteCallbackList<IMessageListener> mRemoteListeners = new RemoteCallbackList<IMessageListener>();
     private final MsgListener mMsgListener = new MsgListener();
+    private BeemOtrManager mOtrManager;
 
     /**
      * Constructor.
@@ -84,6 +86,8 @@
 	mParticipant = new Contact(chat.getParticipant());
 	mMessages = new LinkedList<Message>();
 	mAdaptee.addMessageListener(mMsgListener);
+	mOtrManager = new BeemOtrManager(this, mParticipant.getJIDWithRes(), mParticipant.getJID());
+	Log.d(TAG, "new chat, with otr " + mOtrManager.status());
     }
 
     /**
@@ -100,9 +104,15 @@
     @Override
     public void sendMessage(com.beem.project.beem.service.Message message) throws RemoteException {
 	org.jivesoftware.smack.packet.Message send = new org.jivesoftware.smack.packet.Message();
+	String msgBody = message.getBody();
 	send.setTo(message.getTo());
 	Log.w(TAG, "message to " + message.getTo());
-	send.setBody(message.getBody());
+	if (mOtrManager != null) {
+		Log.d(TAG, "msg out, with otr " + mOtrManager.status());
+		msgBody = mOtrManager.sendMessage(msgBody);
+	}
+		
+	send.setBody(msgBody);
 	send.setThread(message.getThread());
 	send.setSubject(message.getSubject());
 	send.setType(org.jivesoftware.smack.packet.Message.Type.chat);
@@ -116,6 +126,16 @@
 	    e.printStackTrace();
 	}
     }
+    
+    public void sendMessage(String msg) {
+    	Message msgToSend = new Message(mParticipant.getJIDWithRes(), Message.MSG_TYPE_CHAT);
+	    msgToSend.setBody(msg);
+	    try {
+			sendMessage(msgToSend);
+		} catch (RemoteException e) {
+			e.printStackTrace();
+		}
+    }
 
     /**
      * {@inheritDoc}
@@ -206,6 +226,12 @@
 	@Override
 	public void processMessage(Chat chat, org.jivesoftware.smack.packet.Message message) {
 	    Message  msg = new Message(message);
+	    Log.d(TAG, "new msg " + msg.getBody());
+	    if (mOtrManager != null) {
+			Log.d(TAG, "msg in, with otr " + mOtrManager.status());
+			msg.setBody(mOtrManager.recieveMessage(msg.getBody()));
+		}
+	    	
 	    //TODO add que les message pas de type errors
 	    ChatAdapter.this.addMessage(msg);
 	    final int n = mRemoteListeners.beginBroadcast();
@@ -240,5 +266,16 @@
 	    mRemoteListeners.finishBroadcast();
 	}
     }
+
+	@Override
+	public void startOtrSession() throws RemoteException {
+		mOtrManager.startSession();
+		Log.d(TAG, "start otr " + mOtrManager.status());
+	}
+
+	@Override
+	public void endOtrSession() throws RemoteException {
+		mOtrManager.endSession();
+	}
 }
 
--- a/src/com/beem/project/beem/service/aidl/IChat.aidl	Sun Dec 05 18:45:47 2010 +0100
+++ b/src/com/beem/project/beem/service/aidl/IChat.aidl	Sun Dec 05 18:45:54 2010 +0100
@@ -85,5 +85,15 @@
 	void setState(in String state);
 
 	List<Message> getMessages();
+	
+	/**
+	 * Try to start an OTR session for the current chat.
+	 */
+	void startOtrSession();
+	
+	/**
+	 * Stop the OTR session of the current chat.
+	 */
+	void endOtrSession();
 
 }
--- a/src/com/beem/project/beem/service/aidl/IMessageListener.aidl	Sun Dec 05 18:45:47 2010 +0100
+++ b/src/com/beem/project/beem/service/aidl/IMessageListener.aidl	Sun Dec 05 18:45:54 2010 +0100
@@ -61,4 +61,11 @@
 	 * @param chat the chat changed.
 	 */
 	void stateChanged(in IChat chat);
+	
+	/**
+	 * This method is executed when the otr session status change.
+	 * @param otrState the new state of otr session.
+	 */
+	void otrStateChanged(in String otrState);
+	
 }
--- a/src/com/beem/project/beem/ui/Chat.java	Sun Dec 05 18:45:47 2010 +0100
+++ b/src/com/beem/project/beem/ui/Chat.java	Sun Dec 05 18:45:54 2010 +0100
@@ -117,6 +117,7 @@
     private TextView mContactNameTextView;
     private TextView mContactStatusMsgTextView;
     private TextView mContactChatState;
+    private TextView mContactOtrState;
     private ImageView mContactStatusIcon;
     private ListView mMessagesListView;
     private EditText mInputField;
@@ -157,6 +158,7 @@
 	mContactNameTextView = (TextView) findViewById(R.id.chat_contact_name);
 	mContactStatusMsgTextView = (TextView) findViewById(R.id.chat_contact_status_msg);
 	mContactChatState = (TextView) findViewById(R.id.chat_contact_chat_state);
+	mContactOtrState = (TextView) findViewById(R.id.chat_contact_otr_state);
 	mContactStatusIcon = (ImageView) findViewById(R.id.chat_contact_status_icon);
 	mMessagesListView = (ListView) findViewById(R.id.chat_messages);
 	mMessagesListView.setAdapter(mMessagesListAdapter);
@@ -286,6 +288,36 @@
 		}
 		this.finish();
 		break;
+	    case R.id.chat_menu_start_otr_session:
+			try {
+				Log.d(TAG, "opened chats = " + mChat + " for "+mContact);
+				if (mChat == null) {
+					mChat = mChatManager.createChat(mContact, mMessageListener);
+					if (mChat != null) {
+					    mChat.setOpen(true);
+					}
+					
+				}
+			   mChat.startOtrSession();
+			} catch (RemoteException e) {
+			    Log.e(TAG, e.getMessage());
+			}
+			break;
+	    case R.id.chat_menu_stop_otr_session:
+			try {
+				Log.d(TAG, "opened chats = " + mChat + " for "+mContact);
+				if (mChat == null) {
+				    mChat = mChatManager.createChat(mContact, mMessageListener);
+					if (mChat != null) {
+					    mChat.setOpen(true);
+					}
+					
+				}
+			   mChat.endOtrSession();
+			} catch (RemoteException e) {
+			    Log.e(TAG, e.getMessage());
+			}
+			break;
 	    default:
 		return false;
 	}
@@ -537,6 +569,25 @@
 	    });
 
 	}
+
+	@Override
+	public void otrStateChanged(final String otrState) throws RemoteException {
+	    mHandler.post(new Runnable() {
+		@Override
+		public void run() {
+		    String text = null;
+		    if ("PLAINTEXT".equals(otrState)) {
+			text = Chat.this.getString(R.string.chat_state_active);
+		    } else if ("ENCRYPTED".equals(otrState)) {
+			text = Chat.this.getString(R.string.chat_state_composing);
+		    } else if ("FINISHED".equals(otrState)) {
+			text = Chat.this.getString(R.string.chat_state_active);
+		    }
+		    mContactOtrState.setText(text);
+		}
+	    });
+		
+	}
     }
 
     /**
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/com/zadov/beem/BeemOtrManager.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,62 @@
+package com.zadov.beem;
+
+import net.java.otr4j.OtrEngineHostImpl;
+import net.java.otr4j.OtrEngineImpl;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.session.SessionID;
+import android.util.Log;
+
+import com.beem.project.beem.service.ChatAdapter;
+
+public class BeemOtrManager {
+
+	private static final String TAG = "BeemOtrManager";
+	protected ChatAdapter mChat;
+	protected OtrEngineHostImpl myHost;
+	private OtrEngineImpl usAlice;
+	private SessionID aliceSessionID;
+
+	public BeemOtrManager(ChatAdapter chat, String jidres, String jid){
+		mChat = chat;
+		myHost = new OtrEngineHostImpl(chat, 
+				new OtrPolicyImpl(OtrPolicy.ALLOW_V2
+						| OtrPolicy.ERROR_START_AKE));
+		aliceSessionID = new SessionID(jidres,jid, "XMMP");
+		usAlice = new OtrEngineImpl(myHost);
+	}
+
+	public void startSession() {
+		usAlice.startSession(aliceSessionID);
+	}
+	
+	public void endSession() {
+		usAlice.endSession(aliceSessionID);
+	}
+
+	public String status() {
+		return usAlice.getSessionStatus(aliceSessionID).toString();
+	}
+	
+	public String recieveMessage(String msg) {
+		Log.d(TAG, "in: "+msg);
+		String plain = null;
+		//mChat.otrStatusDisplay.setText("otr status: "+usAlice.getSessionStatus(aliceSessionID).toString());
+		if(usAlice != null){
+			plain = usAlice.transformReceiving(aliceSessionID, msg);
+		}
+		Log.d(TAG,"in: "+plain);
+		return plain;
+	}
+	
+	public String sendMessage(String msg) {
+		Log.d(TAG, "out: "+msg);
+		//mChat.otrStatusDisplay.setText("otr status: "+usAlice.getSessionStatus(aliceSessionID).toString());
+		if(usAlice != null){
+			msg = usAlice.transformSending(aliceSessionID, msg);
+		}
+		Log.d(TAG, "out: "+msg);
+		return msg;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrEngine.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,79 @@
+package net.java.otr4j;
+
+import java.security.PublicKey;
+
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionStatus;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public interface OtrEngine {
+
+	/**
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 * @param content
+	 *            The message content to be transformed.
+	 * @return The transformed message content.
+	 */
+	public abstract String transformReceiving(SessionID sessionID,
+			String content);
+
+	/**
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 * @param content
+	 *            The message content to be transformed.
+	 * @return The transformed message content.
+	 */
+	public abstract String transformSending(SessionID sessionID, String content);
+
+	/**
+	 * Starts an Off-the-Record session, if there is no active one.
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 */
+	public abstract void startSession(SessionID sessionID);
+
+	/**
+	 * Ends the Off-the-Record session, if exists.
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 */
+	public abstract void endSession(SessionID sessionID);
+
+	/**
+	 * Stops/Starts the Off-the-Record session.
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 */
+	public abstract void refreshSession(SessionID sessionID);
+
+	/**
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 * @return The status of an Off-the-Record session.
+	 */
+	public abstract SessionStatus getSessionStatus(SessionID sessionID);
+
+	/**
+	 * 
+	 * @param sessionID
+	 *            The session identifier.
+	 * @return The remote public key.
+	 */
+	public abstract PublicKey getRemotePublicKey(SessionID sessionID);
+
+	public abstract void addOtrEngineListener(OtrEngineListener l);
+
+	public abstract void removeOtrEngineListener(OtrEngineListener l);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrEngineHost.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,31 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j;
+
+import java.security.KeyPair;
+
+import net.java.otr4j.session.SessionID;
+
+/**
+ * 
+ * This interface should be implemented by the host application. It is required
+ * for otr4j to work properly.
+ * 
+ * @author George Politis
+ * 
+ */
+public abstract interface OtrEngineHost {
+	public abstract void injectMessage(SessionID sessionID, String msg);
+
+	public abstract void showWarning(SessionID sessionID, String warning);
+
+	public abstract void showError(SessionID sessionID, String error);
+
+	public abstract OtrPolicy getSessionPolicy(SessionID sessionID);
+	
+	public abstract KeyPair getKeyPair(SessionID sessionID);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrEngineHostImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,68 @@
+package net.java.otr4j;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+
+import net.java.otr4j.crypto.KeyAndSession;
+import net.java.otr4j.session.SessionID;
+
+import com.beem.project.beem.service.ChatAdapter;
+
+
+public class OtrEngineHostImpl implements OtrEngineHost{
+	
+	private ChatAdapter mChat;
+	private OtrPolicy policy;
+    public String lastInjectedMessage;
+    private ArrayList<KeyAndSession> keyring = new ArrayList<KeyAndSession>();
+    
+	public OtrEngineHostImpl(ChatAdapter chat, OtrPolicy policy){
+		mChat = chat;
+		this.policy = policy;
+	}
+	
+	@Override
+	public KeyPair getKeyPair(SessionID sessionID) {
+		 
+		for(int i = 0; i<keyring.size(); i++){
+			if(sessionID.equals(keyring.get(i).getSessionID())){
+				return keyring.get(i).getKeyPair();
+			}
+		}
+		
+		 KeyPairGenerator kg;
+         try {
+                 kg = KeyPairGenerator.getInstance("DSA");
+
+         } catch (NoSuchAlgorithmException e) {
+                 e.printStackTrace();
+                 return null;
+         }
+         KeyAndSession kp = new KeyAndSession(kg.genKeyPair(), sessionID);
+         keyring.add(kp);
+         return kp.getKeyPair();
+	}
+
+	@Override
+	public OtrPolicy getSessionPolicy(SessionID sessionID) {
+		return this.policy;
+	}
+
+	@Override
+	public void injectMessage(SessionID sessionID, String msg) {
+		mChat.sendMessage(msg);
+	}
+
+	@Override
+	public void showError(SessionID sessionID, String error) {
+	//	mChat.otrStatusDisplay.setText(""+error);		
+	}
+
+	@Override
+	public void showWarning(SessionID sessionID, String warning) {
+		//mChat.otrStatusDisplay.setText(""+warning);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrEngineImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,136 @@
+/*
+ * otr4j, the open source java otr librar
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+
+package net.java.otr4j;
+
+import java.security.PublicKey;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public class OtrEngineImpl implements OtrEngine {
+
+	public OtrEngineImpl(OtrEngineHost listener) {
+		if (listener == null)
+			throw new IllegalArgumentException("OtrEgineHost is required.");
+
+		this.setListener(listener);
+	}
+
+	private OtrEngineHost listener;
+	private Map<SessionID, Session> sessions;
+
+	private Session getSession(SessionID sessionID) {
+
+		if (sessionID == null || sessionID.equals(SessionID.Empty))
+			throw new IllegalArgumentException();
+
+		if (sessions == null)
+			sessions = new Hashtable<SessionID, Session>();
+
+		if (!sessions.containsKey(sessionID)) {
+			Session session = new SessionImpl(sessionID, getListener());
+			sessions.put(sessionID, session);
+
+			session.addOtrEngineListener(new OtrEngineListener() {
+
+				public void sessionStatusChanged(SessionID sessionID) {
+					for (OtrEngineListener l : listeners)
+						l.sessionStatusChanged(sessionID);
+				}
+			});
+
+		}
+
+		return sessions.get(sessionID);
+	}
+
+	public SessionStatus getSessionStatus(SessionID sessionID) {
+		return this.getSession(sessionID).getSessionStatus();
+	}
+
+	public String transformReceiving(SessionID sessionID, String msgText) {
+		try {
+			return this.getSession(sessionID).transformReceiving(msgText);
+		} catch (OtrException e) {
+			listener.showError(sessionID, e.getMessage());
+			return null;
+		}
+	}
+
+	public String transformSending(SessionID sessionID, String msgText) {
+		try {
+			return this.getSession(sessionID).transformSending(msgText, null);
+		} catch (OtrException e) {
+			listener.showError(sessionID, e.getMessage());
+			return null;
+		}
+	}
+
+	public void endSession(SessionID sessionID) {
+		try {
+			this.getSession(sessionID).endSession();
+		} catch (OtrException e) {
+			listener.showError(sessionID, e.getMessage());
+		}
+	}
+
+	public void startSession(SessionID sessionID) {
+		try {
+			this.getSession(sessionID).startSession();
+		} catch (OtrException e) {
+			listener.showError(sessionID, e.getMessage());
+		}
+	}
+
+	private void setListener(OtrEngineHost listener) {
+		this.listener = listener;
+	}
+
+	private OtrEngineHost getListener() {
+		return listener;
+	}
+
+	public void refreshSession(SessionID sessionID) {
+		try {
+			this.getSession(sessionID).refreshSession();
+		} catch (OtrException e) {
+			listener.showError(sessionID, e.getMessage());
+		}
+	}
+
+	public PublicKey getRemotePublicKey(SessionID sessionID) {
+		return this.getSession(sessionID).getRemotePublicKey();
+	}
+
+	private List<OtrEngineListener> listeners = new Vector<OtrEngineListener>();
+
+	public void addOtrEngineListener(OtrEngineListener l) {
+		synchronized (listeners) {
+			if (!listeners.contains(l))
+				listeners.add(l);
+		}
+
+	}
+
+	public void removeOtrEngineListener(OtrEngineListener l) {
+		synchronized (listeners) {
+			listeners.remove(l);
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrEngineListener.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,14 @@
+package net.java.otr4j;
+
+import net.java.otr4j.session.SessionID;
+
+/**
+ * This interface should be implemented by the host application. It notifies
+ * about session status changes.
+ * 
+ * @author George Politis
+ * 
+ */
+public interface OtrEngineListener {
+	public abstract void sessionStatusChanged(SessionID sessionID);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrException.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,8 @@
+package net.java.otr4j;
+
+@SuppressWarnings("serial")
+public class OtrException extends Exception {
+	public OtrException(Exception e){
+		super(e);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrKeyManager.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,31 @@
+package net.java.otr4j;
+
+import java.security.KeyPair;
+import java.security.PublicKey;
+
+import net.java.otr4j.session.SessionID;
+
+public abstract interface OtrKeyManager {
+
+	public abstract void addListener(OtrKeyManagerListener l);
+
+	public abstract void removeListener(OtrKeyManagerListener l);
+
+	public abstract void verify(SessionID sessionID);
+
+	public abstract void unverify(SessionID sessionID);
+
+	public abstract boolean isVerified(SessionID sessionID);
+
+	public abstract String getRemoteFingerprint(SessionID sessionID);
+
+	public abstract String getLocalFingerprint(SessionID sessionID);
+
+	public abstract void savePublicKey(SessionID sessionID, PublicKey pubKey);
+
+	public abstract PublicKey loadRemotePublicKey(SessionID sessionID);
+
+	public abstract KeyPair loadLocalKeyPair(SessionID sessionID);
+
+	public abstract void generateLocalKeyPair(SessionID sessionID);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrKeyManagerImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,302 @@
+package net.java.otr4j;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.List;
+import java.util.Properties;
+import java.util.Vector;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+
+public class OtrKeyManagerImpl implements OtrKeyManager {
+
+	private OtrKeyManagerStore store;
+
+	public OtrKeyManagerImpl(OtrKeyManagerStore store) {
+		this.store = store;
+	}
+
+	class DefaultPropertiesStore implements OtrKeyManagerStore {
+		private final Properties properties = new Properties();
+		private String filepath;
+
+		public DefaultPropertiesStore(String filepath) throws IOException {
+			if (filepath == null || filepath.length() < 1)
+				throw new IllegalArgumentException();
+			this.filepath = filepath;
+			properties.clear();
+
+			InputStream in = new BufferedInputStream(new FileInputStream(
+					getConfigurationFile()));
+			try {
+				properties.load(in);
+			} finally {
+				in.close();
+			}
+		}
+
+		private File getConfigurationFile() throws IOException {
+			File configFile = new File(filepath);
+			if (!configFile.exists())
+				configFile.createNewFile();
+			return configFile;
+		}
+
+		public void setProperty(String id, boolean value) {
+			properties.setProperty(id, "true");
+			try {
+				this.store();
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+
+		private void store() throws FileNotFoundException, IOException {
+			OutputStream out = new FileOutputStream(getConfigurationFile());
+			properties.store(out, null);
+			out.close();
+		}
+
+		public void setProperty(String id, byte[] value) {
+			properties.setProperty(id, new String(Base64.encode(value)));
+			try {
+				this.store();
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+		}
+
+		public void removeProperty(String id) {
+			properties.remove(id);
+
+		}
+
+		public byte[] getPropertyBytes(String id) {
+			String value = properties.getProperty(id);
+			return Base64.decode(value);
+		}
+
+		public boolean getPropertyBoolean(String id, boolean defaultValue) {
+			try {
+				return Boolean.valueOf(properties.get(id).toString());
+			} catch (Exception e) {
+				return defaultValue;
+			}
+		}
+	}
+
+	public OtrKeyManagerImpl(String filepath) throws IOException {
+		this.store = new DefaultPropertiesStore(filepath);
+	}
+
+	private List<OtrKeyManagerListener> listeners = new Vector<OtrKeyManagerListener>();
+
+	public void addListener(OtrKeyManagerListener l) {
+		synchronized (listeners) {
+			if (!listeners.contains(l))
+				listeners.add(l);
+		}
+	}
+
+	public void removeListener(OtrKeyManagerListener l) {
+		synchronized (listeners) {
+			listeners.remove(l);
+		}
+	}
+
+	public void generateLocalKeyPair(SessionID sessionID) {
+		if (sessionID == null)
+			return;
+
+		String accountID = sessionID.getAccountID();
+		KeyPair keyPair;
+		try {
+			keyPair = KeyPairGenerator.getInstance("DSA").genKeyPair();
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+			return;
+		}
+
+		// Store Public Key.
+		PublicKey pubKey = keyPair.getPublic();
+		X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(pubKey
+				.getEncoded());
+
+		this.store.setProperty(accountID + ".publicKey", x509EncodedKeySpec
+				.getEncoded());
+
+		// Store Private Key.
+		PrivateKey privKey = keyPair.getPrivate();
+		PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(
+				privKey.getEncoded());
+
+		this.store.setProperty(accountID + ".privateKey", pkcs8EncodedKeySpec
+				.getEncoded());
+	}
+
+	public String getLocalFingerprint(SessionID sessionID) {
+		KeyPair keyPair = loadLocalKeyPair(sessionID);
+
+		if (keyPair == null)
+			return null;
+
+		PublicKey pubKey = keyPair.getPublic();
+
+		try {
+			return new OtrCryptoEngineImpl().getFingerprint(pubKey);
+		} catch (OtrCryptoException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	public String getRemoteFingerprint(SessionID sessionID) {
+		PublicKey remotePublicKey = loadRemotePublicKey(sessionID);
+		if (remotePublicKey == null)
+			return null;
+		try {
+			return new OtrCryptoEngineImpl().getFingerprint(remotePublicKey);
+		} catch (OtrCryptoException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	public boolean isVerified(SessionID sessionID) {
+		if (sessionID == null)
+			return false;
+
+		return this.store.getPropertyBoolean(sessionID.getUserID()
+				+ ".publicKey.verified", false);
+	}
+
+	public KeyPair loadLocalKeyPair(SessionID sessionID) {
+		if (sessionID == null)
+			return null;
+
+		String accountID = sessionID.getAccountID();
+		// Load Private Key.
+		byte[] b64PrivKey = this.store.getPropertyBytes(accountID
+				+ ".privateKey");
+		if (b64PrivKey == null)
+			return null;
+
+		PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(b64PrivKey);
+
+		// Load Public Key.
+		byte[] b64PubKey = this.store
+				.getPropertyBytes(accountID + ".publicKey");
+		if (b64PubKey == null)
+			return null;
+
+		X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(b64PubKey);
+
+		PublicKey publicKey;
+		PrivateKey privateKey;
+
+		// Generate KeyPair.
+		KeyFactory keyFactory;
+		try {
+			keyFactory = KeyFactory.getInstance("DSA");
+			publicKey = keyFactory.generatePublic(publicKeySpec);
+			privateKey = keyFactory.generatePrivate(privateKeySpec);
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+			return null;
+		} catch (InvalidKeySpecException e) {
+			e.printStackTrace();
+			return null;
+		}
+
+		return new KeyPair(publicKey, privateKey);
+	}
+
+	public PublicKey loadRemotePublicKey(SessionID sessionID) {
+		if (sessionID == null)
+			return null;
+
+		String userID = sessionID.getUserID();
+
+		byte[] b64PubKey = this.store.getPropertyBytes(userID + ".publicKey");
+		if (b64PubKey == null)
+			return null;
+
+		X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(b64PubKey);
+
+		// Generate KeyPair.
+		KeyFactory keyFactory;
+		try {
+			keyFactory = KeyFactory.getInstance("DSA");
+			return keyFactory.generatePublic(publicKeySpec);
+		} catch (NoSuchAlgorithmException e) {
+			e.printStackTrace();
+			return null;
+		} catch (InvalidKeySpecException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+
+	public void savePublicKey(SessionID sessionID, PublicKey pubKey) {
+		if (sessionID == null)
+			return;
+
+		X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(pubKey
+				.getEncoded());
+
+		String userID = sessionID.getUserID();
+		this.store.setProperty(userID + ".publicKey", x509EncodedKeySpec
+				.getEncoded());
+
+		this.store.removeProperty(userID + ".publicKey.verified");
+	}
+
+	public void unverify(SessionID sessionID) {
+		if (sessionID == null)
+			return;
+
+		if (!isVerified(sessionID))
+			return;
+
+		this.store
+				.removeProperty(sessionID.getUserID() + ".publicKey.verified");
+
+		for (OtrKeyManagerListener l : listeners)
+			l.verificationStatusChanged(sessionID);
+
+	}
+
+	public void verify(SessionID sessionID) {
+		if (sessionID == null)
+			return;
+
+		if (this.isVerified(sessionID))
+			return;
+
+		this.store.setProperty(sessionID.getUserID() + ".publicKey.verified",
+				true);
+
+		for (OtrKeyManagerListener l : listeners)
+			l.verificationStatusChanged(sessionID);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrKeyManagerListener.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,7 @@
+package net.java.otr4j;
+
+import net.java.otr4j.session.SessionID;
+
+public interface OtrKeyManagerListener {
+	public abstract void verificationStatusChanged(SessionID session);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrKeyManagerStore.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,13 @@
+package net.java.otr4j;
+
+public interface OtrKeyManagerStore {
+	public abstract byte[] getPropertyBytes(String id);
+
+	public abstract boolean getPropertyBoolean(String id, boolean defaultValue);
+
+	public abstract void setProperty(String id, byte[] value);
+
+	public abstract void setProperty(String id, boolean value);
+
+	public abstract void removeProperty(String id);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrPolicy.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,68 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public interface OtrPolicy {
+
+	public static final int ALLOW_V1 = 0x01;
+	public static final int ALLOW_V2 = 0x02;
+	public static final int REQUIRE_ENCRYPTION = 0x04;
+	public static final int SEND_WHITESPACE_TAG = 0x08;
+	public static final int WHITESPACE_START_AKE = 0x10;
+	public static final int ERROR_START_AKE = 0x20;
+	public static final int VERSION_MASK = (ALLOW_V1 | ALLOW_V2);
+
+	// The four old version 1 policies correspond to the following combinations
+	// of flags (adding an allowance for version 2 of the protocol):
+
+	public static final int NEVER = 0x00;
+	public static final int OPPORTUNISTIC = (ALLOW_V1 | ALLOW_V2
+			| SEND_WHITESPACE_TAG | WHITESPACE_START_AKE | ERROR_START_AKE);
+	public static final int OTRL_POLICY_MANUAL = (ALLOW_V1 | ALLOW_V2);
+	public static final int OTRL_POLICY_ALWAYS = (ALLOW_V1 | ALLOW_V2
+			| REQUIRE_ENCRYPTION | WHITESPACE_START_AKE | ERROR_START_AKE);
+	public static final int OTRL_POLICY_DEFAULT = OPPORTUNISTIC;
+
+	public abstract boolean getAllowV1();
+
+	public abstract boolean getAllowV2();
+
+	public abstract boolean getRequireEncryption();
+
+	public abstract boolean getSendWhitespaceTag();
+
+	public abstract boolean getWhitespaceStartAKE();
+
+	public abstract boolean getErrorStartAKE();
+
+	public abstract int getPolicy();
+
+	public abstract void setAllowV1(boolean value);
+
+	public abstract void setAllowV2(boolean value);
+
+	public abstract void setRequireEncryption(boolean value);
+
+	public abstract void setSendWhitespaceTag(boolean value);
+
+	public abstract void setWhitespaceStartAKE(boolean value);
+
+	public abstract void setErrorStartAKE(boolean value);
+
+	public abstract void setEnableAlways(boolean value);
+
+	public abstract boolean getEnableAlways();
+
+	public abstract void setEnableManual(boolean value);
+
+	public abstract boolean getEnableManual();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/OtrPolicyImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,128 @@
+package net.java.otr4j;
+
+public class OtrPolicyImpl implements OtrPolicy {
+
+	public OtrPolicyImpl() {
+		this.setPolicy(NEVER);
+	}
+
+	public OtrPolicyImpl(int policy) {
+		this.setPolicy(policy);
+	}
+
+	private int policy;
+
+	public int getPolicy() {
+		return policy;
+	}
+
+	private void setPolicy(int policy) {
+		this.policy = policy;
+	}
+
+	public boolean getAllowV1() {
+		return (policy & OtrPolicy.ALLOW_V1) != 0;
+	}
+
+	public boolean getAllowV2() {
+		return (policy & OtrPolicy.ALLOW_V2) != 0;
+	}
+
+	public boolean getErrorStartAKE() {
+		return (policy & OtrPolicy.ERROR_START_AKE) != 0;
+	}
+
+	public boolean getRequireEncryption() {
+		return getEnableManual()
+				&& (policy & OtrPolicy.REQUIRE_ENCRYPTION) != 0;
+	}
+
+	public boolean getSendWhitespaceTag() {
+		return (policy & OtrPolicy.SEND_WHITESPACE_TAG) != 0;
+	}
+
+	public boolean getWhitespaceStartAKE() {
+		return (policy & OtrPolicy.WHITESPACE_START_AKE) != 0;
+	}
+
+	public void setAllowV1(boolean value) {
+		if (value)
+			policy |= ALLOW_V1;
+		else
+			policy &= ~ALLOW_V1;
+	}
+
+	public void setAllowV2(boolean value) {
+		if (value)
+			policy |= ALLOW_V2;
+		else
+			policy &= ~ALLOW_V2;
+	}
+
+	public void setErrorStartAKE(boolean value) {
+		if (value)
+			policy |= ERROR_START_AKE;
+		else
+			policy &= ~ERROR_START_AKE;
+	}
+
+	public void setRequireEncryption(boolean value) {
+		if (value)
+			policy |= REQUIRE_ENCRYPTION;
+		else
+			policy &= ~REQUIRE_ENCRYPTION;
+	}
+
+	public void setSendWhitespaceTag(boolean value) {
+		if (value)
+			policy |= SEND_WHITESPACE_TAG;
+		else
+			policy &= ~SEND_WHITESPACE_TAG;
+	}
+
+	public void setWhitespaceStartAKE(boolean value) {
+		if (value)
+			policy |= WHITESPACE_START_AKE;
+		else
+			policy &= ~WHITESPACE_START_AKE;
+	}
+
+	public boolean getEnableAlways() {
+		return getEnableManual() && getErrorStartAKE()
+				&& getSendWhitespaceTag() && getWhitespaceStartAKE();
+	}
+
+	public void setEnableAlways(boolean value) {
+		if (value)
+			setEnableManual(true);
+
+		setErrorStartAKE(value);
+		setSendWhitespaceTag(value);
+		setWhitespaceStartAKE(value);
+
+	}
+
+	public boolean getEnableManual() {
+		return getAllowV1() && getAllowV2();
+	}
+
+	public void setEnableManual(boolean value) {
+		setAllowV1(value);
+		setAllowV2(value);
+	}
+
+	public boolean equals(Object obj) {
+		if (obj == this)
+			return true;
+		if (obj == null || obj.getClass() != this.getClass())
+			return false;
+
+		OtrPolicy policy = (OtrPolicy) obj;
+
+		return policy.getPolicy() == this.getPolicy();
+	}
+
+	public int hashCode() {
+		return this.getPolicy();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/crypto/KeyAndSession.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,23 @@
+package net.java.otr4j.crypto;
+
+import java.security.KeyPair;
+
+import net.java.otr4j.session.SessionID;
+
+public class KeyAndSession {
+
+	private KeyPair myKeyPair;
+	private SessionID mySessionID;
+	
+	public KeyAndSession(KeyPair kp, SessionID si){
+		myKeyPair = kp;
+		mySessionID = si;
+	}
+	
+	public KeyPair getKeyPair(){
+		return myKeyPair;
+	}
+	public SessionID getSessionID(){
+		return mySessionID;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/crypto/OtrCryptoEngine.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,83 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+
+package net.java.otr4j.crypto;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public interface OtrCryptoEngine {
+
+	public static final String MODULUS_TEXT = "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF";
+	public static final BigInteger MODULUS = new BigInteger(MODULUS_TEXT, 16);
+	public static final BigInteger BIGINTEGER_TWO = BigInteger.valueOf(2);
+	public static final BigInteger MODULUS_MINUS_TWO = MODULUS
+			.subtract(BIGINTEGER_TWO);
+
+	public static String GENERATOR_TEXT = "2";
+	public static BigInteger GENERATOR = new BigInteger(GENERATOR_TEXT, 10);
+
+	public static final int AES_KEY_BYTE_LENGTH = 16;
+	public static final int SHA256_HMAC_KEY_BYTE_LENGTH = 32;
+	public static final int DH_PRIVATE_KEY_MINIMUM_BIT_LENGTH = 320;
+	public static final byte[] ZERO_CTR = new byte[] { 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00 };
+
+	public static final int DSA_PUB_TYPE = 0;
+
+	public abstract KeyPair generateDHKeyPair() throws OtrCryptoException;
+
+	public abstract DHPublicKey getDHPublicKey(byte[] mpiBytes)
+			throws OtrCryptoException;
+
+	public abstract DHPublicKey getDHPublicKey(BigInteger mpi)
+			throws OtrCryptoException;
+
+	public abstract byte[] sha256Hmac(byte[] b, byte[] key)
+			throws OtrCryptoException;
+
+	public abstract byte[] sha256Hmac(byte[] b, byte[] key, int length)
+			throws OtrCryptoException;
+
+	public abstract byte[] sha1Hmac(byte[] b, byte[] key, int length)
+			throws OtrCryptoException;
+
+	public abstract byte[] sha256Hmac160(byte[] b, byte[] key)
+			throws OtrCryptoException;
+
+	public abstract byte[] sha256Hash(byte[] b) throws OtrCryptoException;
+
+	public abstract byte[] sha1Hash(byte[] b) throws OtrCryptoException;
+
+	public abstract byte[] aesDecrypt(byte[] key, byte[] ctr, byte[] b)
+			throws OtrCryptoException;
+
+	public abstract byte[] aesEncrypt(byte[] key, byte[] ctr, byte[] b)
+			throws OtrCryptoException;
+
+	public abstract BigInteger generateSecret(PrivateKey privKey,
+			PublicKey pubKey) throws OtrCryptoException;
+
+	public abstract byte[] sign(byte[] b, PrivateKey privatekey)
+			throws OtrCryptoException;
+
+	public abstract boolean verify(byte[] b, PublicKey pubKey, byte[] rs)
+			throws OtrCryptoException;
+
+	public abstract String getFingerprint(PublicKey pubKey)
+			throws OtrCryptoException;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/crypto/OtrCryptoEngineImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,393 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.crypto;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.interfaces.DHPrivateKey;
+import javax.crypto.interfaces.DHPublicKey;
+import javax.crypto.spec.DHPrivateKeySpec;
+import javax.crypto.spec.DHPublicKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import net.java.otr4j.io.SerializationUtils;
+
+import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
+import org.bouncycastle.crypto.BufferedBlockCipher;
+import org.bouncycastle.crypto.engines.AESFastEngine;
+import org.bouncycastle.crypto.generators.DHKeyPairGenerator;
+import org.bouncycastle.crypto.modes.SICBlockCipher;
+import org.bouncycastle.crypto.params.DHKeyGenerationParameters;
+import org.bouncycastle.crypto.params.DHParameters;
+import org.bouncycastle.crypto.params.DHPrivateKeyParameters;
+import org.bouncycastle.crypto.params.DHPublicKeyParameters;
+import org.bouncycastle.crypto.params.DSAParameters;
+import org.bouncycastle.crypto.params.DSAPrivateKeyParameters;
+import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
+import org.bouncycastle.crypto.params.KeyParameter;
+import org.bouncycastle.crypto.params.ParametersWithIV;
+import org.bouncycastle.crypto.signers.DSASigner;
+import org.bouncycastle.util.BigIntegers;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public class OtrCryptoEngineImpl implements OtrCryptoEngine {
+
+	public KeyPair generateDHKeyPair() throws OtrCryptoException {
+
+		// Generate a AsymmetricCipherKeyPair using BC.
+		DHParameters dhParams = new DHParameters(MODULUS, GENERATOR, null,
+				DH_PRIVATE_KEY_MINIMUM_BIT_LENGTH);
+		DHKeyGenerationParameters params = new DHKeyGenerationParameters(
+				new SecureRandom(), dhParams);
+		DHKeyPairGenerator kpGen = new DHKeyPairGenerator();
+
+		kpGen.init(params);
+		AsymmetricCipherKeyPair pair = kpGen.generateKeyPair();
+
+		// Convert this AsymmetricCipherKeyPair to a standard JCE KeyPair.
+		DHPublicKeyParameters pub = (DHPublicKeyParameters) pair.getPublic();
+		DHPrivateKeyParameters priv = (DHPrivateKeyParameters) pair
+				.getPrivate();
+
+		try {
+			KeyFactory keyFac = KeyFactory.getInstance("DH");
+
+			DHPublicKeySpec pubKeySpecs = new DHPublicKeySpec(pub.getY(),
+					MODULUS, GENERATOR);
+			DHPublicKey pubKey = (DHPublicKey) keyFac
+					.generatePublic(pubKeySpecs);
+
+			DHParameters dhParameters = priv.getParameters();
+			DHPrivateKeySpec privKeySpecs = new DHPrivateKeySpec(priv.getX(),
+					dhParameters.getP(), dhParameters.getG());
+			DHPrivateKey privKey = (DHPrivateKey) keyFac
+					.generatePrivate(privKeySpecs);
+
+			return new KeyPair(pubKey, privKey);
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public DHPublicKey getDHPublicKey(byte[] mpiBytes)
+			throws OtrCryptoException {
+		return getDHPublicKey(new BigInteger(mpiBytes));
+	}
+
+	public DHPublicKey getDHPublicKey(BigInteger mpi) throws OtrCryptoException {
+		DHPublicKeySpec pubKeySpecs = new DHPublicKeySpec(mpi, MODULUS,
+				GENERATOR);
+		try {
+			KeyFactory keyFac = KeyFactory.getInstance("DH");
+			return (DHPublicKey) keyFac.generatePublic(pubKeySpecs);
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public byte[] sha256Hmac(byte[] b, byte[] key) throws OtrCryptoException {
+		return this.sha256Hmac(b, key, 0);
+	}
+
+	public byte[] sha256Hmac(byte[] b, byte[] key, int length)
+			throws OtrCryptoException {
+
+		SecretKeySpec keyspec = new SecretKeySpec(key, "HmacSHA256");
+		javax.crypto.Mac mac;
+		try {
+			mac = javax.crypto.Mac.getInstance("HmacSHA256");
+		} catch (NoSuchAlgorithmException e) {
+			throw new OtrCryptoException(e);
+		}
+		try {
+			mac.init(keyspec);
+		} catch (InvalidKeyException e) {
+			throw new OtrCryptoException(e);
+		}
+
+		byte[] macBytes = mac.doFinal(b);
+
+		if (length > 0) {
+			byte[] bytes = new byte[length];
+			ByteBuffer buff = ByteBuffer.wrap(macBytes);
+			buff.get(bytes);
+			return bytes;
+		} else {
+			return macBytes;
+		}
+	}
+
+	public byte[] sha1Hmac(byte[] b, byte[] key, int length)
+			throws OtrCryptoException {
+
+		try {
+			SecretKeySpec keyspec = new SecretKeySpec(key, "HmacSHA1");
+			javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA1");
+			mac.init(keyspec);
+
+			byte[] macBytes = mac.doFinal(b);
+
+			if (length > 0) {
+				byte[] bytes = new byte[length];
+				ByteBuffer buff = ByteBuffer.wrap(macBytes);
+				buff.get(bytes);
+				return bytes;
+			} else {
+				return macBytes;
+			}
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public byte[] sha256Hmac160(byte[] b, byte[] key) throws OtrCryptoException {
+		return sha256Hmac(b, key, 20);
+	}
+
+	public byte[] sha256Hash(byte[] b) throws OtrCryptoException {
+		try {
+			MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
+			sha256.update(b, 0, b.length);
+			return sha256.digest();
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public byte[] sha1Hash(byte[] b) throws OtrCryptoException {
+		try {
+			MessageDigest sha256 = MessageDigest.getInstance("SHA-1");
+			sha256.update(b, 0, b.length);
+			return sha256.digest();
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public byte[] aesDecrypt(byte[] key, byte[] ctr, byte[] b)
+			throws OtrCryptoException {
+
+		AESFastEngine aesDec = new AESFastEngine();
+		SICBlockCipher sicAesDec = new SICBlockCipher(aesDec);
+		BufferedBlockCipher bufSicAesDec = new BufferedBlockCipher(sicAesDec);
+
+		// Create initial counter value 0.
+		if (ctr == null)
+			ctr = ZERO_CTR;
+		bufSicAesDec.init(false, new ParametersWithIV(new KeyParameter(key),
+				ctr));
+		byte[] aesOutLwDec = new byte[b.length];
+		int done = bufSicAesDec.processBytes(b, 0, b.length, aesOutLwDec, 0);
+		try {
+			bufSicAesDec.doFinal(aesOutLwDec, done);
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+
+		return aesOutLwDec;
+	}
+
+	public byte[] aesEncrypt(byte[] key, byte[] ctr, byte[] b)
+			throws OtrCryptoException {
+
+		AESFastEngine aesEnc = new AESFastEngine();
+		SICBlockCipher sicAesEnc = new SICBlockCipher(aesEnc);
+		BufferedBlockCipher bufSicAesEnc = new BufferedBlockCipher(sicAesEnc);
+
+		// Create initial counter value 0.
+		if (ctr == null)
+			ctr = ZERO_CTR;
+		bufSicAesEnc.init(true,
+				new ParametersWithIV(new KeyParameter(key), ctr));
+		byte[] aesOutLwEnc = new byte[b.length];
+		int done = bufSicAesEnc.processBytes(b, 0, b.length, aesOutLwEnc, 0);
+		try {
+			bufSicAesEnc.doFinal(aesOutLwEnc, done);
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+		return aesOutLwEnc;
+	}
+
+	public BigInteger generateSecret(PrivateKey privKey, PublicKey pubKey)
+			throws OtrCryptoException {
+		try {
+			KeyAgreement ka = KeyAgreement.getInstance("DH");
+			ka.init(privKey);
+			ka.doPhase(pubKey, true);
+			byte[] sb = ka.generateSecret();
+			BigInteger s = new BigInteger(1, sb);
+			return s;
+
+		} catch (Exception e) {
+			throw new OtrCryptoException(e);
+		}
+	}
+
+	public byte[] sign(byte[] b, PrivateKey privatekey)
+			throws OtrCryptoException {
+
+		if (!(privatekey instanceof DSAPrivateKey))
+			throw new IllegalArgumentException();
+
+		DSAParams dsaParams = ((DSAPrivateKey) privatekey).getParams();
+		DSAParameters bcDSAParameters = new DSAParameters(dsaParams.getP(),
+				dsaParams.getQ(), dsaParams.getG());
+
+		DSAPrivateKey dsaPrivateKey = (DSAPrivateKey) privatekey;
+		DSAPrivateKeyParameters bcDSAPrivateKeyParms = new DSAPrivateKeyParameters(
+				dsaPrivateKey.getX(), bcDSAParameters);
+
+		DSASigner dsaSigner = new DSASigner();
+		dsaSigner.init(true, bcDSAPrivateKeyParms);
+
+		BigInteger q = dsaParams.getQ();
+
+		// Ian: Note that if you can get the standard DSA implementation you're
+		// using to not hash its input, you should be able to pass it ((256-bit
+		// value) mod q), (rather than truncating the 256-bit value) and all
+		// should be well.
+		// ref: Interop problems with libotr - DSA signature
+		BigInteger bmpi = new BigInteger(1, b);
+		BigInteger[] rs = dsaSigner.generateSignature(BigIntegers
+				.asUnsignedByteArray(bmpi.mod(q)));
+
+		int siglen = q.bitLength() / 4;
+		int rslen = siglen / 2;
+		byte[] rb = BigIntegers.asUnsignedByteArray(rs[0]);
+		byte[] sb = BigIntegers.asUnsignedByteArray(rs[1]);
+
+		// Create the final signature array, padded with zeros if necessary.
+		byte[] sig = new byte[siglen];
+		Boolean writeR = false;
+		Boolean writeS = false;
+		for (int i = 0; i < siglen; i++) {
+			if (i < rslen) {
+				if (!writeR)
+					writeR = rb.length >= rslen - i;
+				sig[i] = (writeR) ? rb[i] : (byte) 0x0;
+			} else {
+				int j = i - rslen; // Rebase.
+				if (!writeS)
+					writeS = sb.length >= rslen - j;
+				sig[i] = (writeS) ? sb[j] : (byte) 0x0;
+			}
+		}
+		return sig;
+	}
+
+	public boolean verify(byte[] b, PublicKey pubKey, byte[] rs)
+			throws OtrCryptoException {
+
+		if (!(pubKey instanceof DSAPublicKey))
+			throw new IllegalArgumentException();
+
+		DSAParams dsaParams = ((DSAPublicKey) pubKey).getParams();
+		int qlen = dsaParams.getQ().bitLength() / 8;
+		ByteBuffer buff = ByteBuffer.wrap(rs);
+		byte[] r = new byte[qlen];
+		buff.get(r);
+		byte[] s = new byte[qlen];
+		buff.get(s);
+		return verify(b, pubKey, r, s);
+	}
+
+	private Boolean verify(byte[] b, PublicKey pubKey, byte[] r, byte[] s)
+			throws OtrCryptoException {
+		Boolean result = verify(b, pubKey, new BigInteger(1, r),
+				new BigInteger(1, s));
+		return result;
+	}
+
+	private Boolean verify(byte[] b, PublicKey pubKey, BigInteger r,
+			BigInteger s) throws OtrCryptoException {
+
+		if (!(pubKey instanceof DSAPublicKey))
+			throw new IllegalArgumentException();
+
+		DSAParams dsaParams = ((DSAPublicKey) pubKey).getParams();
+
+		BigInteger q = dsaParams.getQ();
+		DSAParameters bcDSAParams = new DSAParameters(dsaParams.getP(), q,
+				dsaParams.getG());
+
+		DSAPublicKey dsaPrivateKey = (DSAPublicKey) pubKey;
+		DSAPublicKeyParameters dsaPrivParms = new DSAPublicKeyParameters(
+				dsaPrivateKey.getY(), bcDSAParams);
+
+		// Ian: Note that if you can get the standard DSA implementation you're
+		// using to not hash its input, you should be able to pass it ((256-bit
+		// value) mod q), (rather than truncating the 256-bit value) and all
+		// should be well.
+		// ref: Interop problems with libotr - DSA signature
+		DSASigner dsaSigner = new DSASigner();
+		dsaSigner.init(false, dsaPrivParms);
+
+		BigInteger bmpi = new BigInteger(1, b);
+		Boolean result = dsaSigner.verifySignature(BigIntegers
+				.asUnsignedByteArray(bmpi.mod(q)), r, s);
+		return result;
+	}
+
+	public String getFingerprint(PublicKey pubKey) throws OtrCryptoException {
+		byte[] b;
+		try {
+			byte[] bRemotePubKey = SerializationUtils.writePublicKey(pubKey);
+
+			if (pubKey.getAlgorithm().equals("DSA")) {
+				byte[] trimmed = new byte[bRemotePubKey.length - 2];
+				System.arraycopy(bRemotePubKey, 2, trimmed, 0, trimmed.length);
+				b = new OtrCryptoEngineImpl().sha1Hash(trimmed);
+			} else
+				b = new OtrCryptoEngineImpl().sha1Hash(bRemotePubKey);
+		} catch (IOException e) {
+			throw new OtrCryptoException(e);
+		}
+		return this.byteArrayToHexString(b);
+	}
+
+	private String byteArrayToHexString(byte in[]) {
+		byte ch = 0x00;
+		int i = 0;
+		if (in == null || in.length <= 0)
+			return null;
+		String pseudo[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
+				"A", "B", "C", "D", "E", "F" };
+		StringBuffer out = new StringBuffer(in.length * 2);
+		while (i < in.length) {
+			ch = (byte) (in[i] & 0xF0);
+			ch = (byte) (ch >>> 4);
+			ch = (byte) (ch & 0x0F);
+			out.append(pseudo[(int) ch]);
+			ch = (byte) (in[i] & 0x0F);
+			out.append(pseudo[(int) ch]);
+			i++;
+		}
+
+		String rslt = new String(out);
+		return rslt;
+
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/crypto/OtrCryptoException.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,12 @@
+package net.java.otr4j.crypto;
+
+import net.java.otr4j.OtrException;
+
+@SuppressWarnings("serial")
+public class OtrCryptoException extends OtrException {
+
+	public OtrCryptoException(Exception e) {
+		super(e);
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/OtrInputStream.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,135 @@
+package net.java.otr4j.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPublicKey;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.io.messages.SignatureX;
+
+public class OtrInputStream extends FilterInputStream implements
+		SerializationConstants {
+
+	public OtrInputStream(InputStream in) {
+		super(in);
+	}
+
+	private int readNumber(int length) throws IOException {
+		byte[] b = new byte[length];
+		read(b);
+
+		int value = 0;
+		for (int i = 0; i < b.length; i++) {
+			int shift = (b.length - 1 - i) * 8;
+			value += (b[i] & 0x000000FF) << shift;
+		}
+
+		return value;
+	}
+
+	public int readByte() throws IOException {
+		return readNumber(TYPE_LEN_BYTE);
+	}
+
+	public int readInt() throws IOException {
+		return readNumber(TYPE_LEN_INT);
+	}
+
+	public int readShort() throws IOException {
+		return readNumber(TYPE_LEN_SHORT);
+	}
+
+	public byte[] readCtr() throws IOException {
+		byte[] b = new byte[TYPE_LEN_CTR];
+		read(b);
+		return b;
+	}
+
+	public byte[] readMac() throws IOException {
+		byte[] b = new byte[TYPE_LEN_MAC];
+		read(b);
+		return b;
+	}
+
+	public BigInteger readBigInt() throws IOException {
+		byte[] b = readData();
+		return new BigInteger(1, b);
+	}
+
+	public byte[] readData() throws IOException {
+		int dataLen = readNumber(DATA_LEN);
+		byte[] b = new byte[dataLen];
+		read(b);
+		return b;
+	}
+
+	public PublicKey readPublicKey() throws IOException {
+		int type = readShort();
+		switch (type) {
+		case 0:
+			BigInteger p = readBigInt();
+			BigInteger q = readBigInt();
+			BigInteger g = readBigInt();
+			BigInteger y = readBigInt();
+			DSAPublicKeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
+			KeyFactory keyFactory;
+			try {
+				keyFactory = KeyFactory.getInstance("DSA");
+			} catch (NoSuchAlgorithmException e) {
+				throw new IOException();
+			}
+			try {
+				return keyFactory.generatePublic(keySpec);
+			} catch (InvalidKeySpecException e) {
+				throw new IOException();
+			}
+		default:
+			throw new UnsupportedOperationException();
+		}
+	}
+
+	public DHPublicKey readDHPublicKey() throws IOException {
+		BigInteger gyMpi = readBigInt();
+		try {
+			return new OtrCryptoEngineImpl().getDHPublicKey(gyMpi);
+		} catch (Exception ex) {
+			throw new IOException();
+		}
+	}
+
+	public byte[] readTlvData() throws IOException {
+		int len = readNumber(TYPE_LEN_BYTE);
+
+		byte[] b = new byte[len];
+		in.read(b);
+		return b;
+	}
+
+	public byte[] readSignature(PublicKey pubKey) throws IOException {
+		if (!pubKey.getAlgorithm().equals("DSA"))
+			throw new UnsupportedOperationException();
+
+		DSAPublicKey dsaPubKey = (DSAPublicKey) pubKey;
+		DSAParams dsaParams = dsaPubKey.getParams();
+		byte[] sig = new byte[dsaParams.getQ().bitLength() / 4];
+		read(sig);
+		return sig;
+	}
+
+	public SignatureX readMysteriousX() throws IOException {
+		PublicKey pubKey = readPublicKey();
+		int dhKeyID = readInt();
+		byte[] sig = readSignature(pubKey);
+		return new SignatureX(pubKey, dhKeyID, sig);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/OtrOutputStream.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,140 @@
+package net.java.otr4j.io;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.security.PublicKey;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPublicKey;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.io.messages.SignatureM;
+import net.java.otr4j.io.messages.MysteriousT;
+import net.java.otr4j.io.messages.SignatureX;
+
+import org.bouncycastle.util.BigIntegers;
+
+public class OtrOutputStream extends FilterOutputStream implements
+		SerializationConstants {
+
+	public OtrOutputStream(OutputStream out) {
+		super(out);
+	}
+
+	private void writeNumber(int value, int length) throws IOException {
+		byte[] b = new byte[length];
+		for (int i = 0; i < length; i++) {
+			int offset = (b.length - 1 - i) * 8;
+			b[i] = (byte) ((value >>> offset) & 0xFF);
+		}
+		write(b);
+	}
+
+	public void writeBigInt(BigInteger bi) throws IOException {
+		byte[] b = BigIntegers.asUnsignedByteArray(bi);
+		writeData(b);
+	}
+
+	public void writeByte(int b) throws IOException {
+		writeNumber(b, TYPE_LEN_BYTE);
+	}
+
+	public void writeData(byte[] b) throws IOException {
+		int len = (b == null || b.length < 0) ? 0 : b.length;
+		writeNumber(len, DATA_LEN);
+		if (len > 0)
+			write(b);
+	}
+
+	public void writeInt(int i) throws IOException {
+		writeNumber(i, TYPE_LEN_INT);
+
+	}
+
+	public void writeShort(int s) throws IOException {
+		writeNumber(s, TYPE_LEN_SHORT);
+
+	}
+
+	public void writeMac(byte[] mac) throws IOException {
+		if (mac == null || mac.length != TYPE_LEN_MAC)
+			throw new IllegalArgumentException();
+
+		write(mac);
+	}
+
+	public void writeCtr(byte[] ctr) throws IOException {
+		if (ctr == null || ctr.length < 1)
+			return;
+
+		int i = 0;
+		while (i < TYPE_LEN_CTR && i < ctr.length) {
+			write(ctr[i]);
+			i++;
+		}
+	}
+
+	public void writeDHPublicKey(DHPublicKey dhPublicKey) throws IOException {
+		byte[] b = BigIntegers.asUnsignedByteArray(dhPublicKey.getY());
+		writeData(b);
+	}
+
+	public void writePublicKey(PublicKey pubKey) throws IOException {
+		if (!(pubKey instanceof DSAPublicKey))
+			throw new UnsupportedOperationException(
+					"Key types other than DSA are not supported at the moment.");
+
+		DSAPublicKey dsaKey = (DSAPublicKey) pubKey;
+
+		writeShort(0);
+
+		DSAParams dsaParams = dsaKey.getParams();
+		writeBigInt(dsaParams.getP());
+		writeBigInt(dsaParams.getQ());
+		writeBigInt(dsaParams.getG());
+		writeBigInt(dsaKey.getY());
+
+	}
+
+	public void writeTlvData(byte[] b) throws IOException {
+		int len = (b == null || b.length < 0) ? 0 : b.length;
+		writeNumber(len, TLV_LEN);
+		if (len > 0)
+			write(b);
+	}
+
+	public void writeSignature(byte[] signature, PublicKey pubKey)
+			throws IOException {
+		if (!pubKey.getAlgorithm().equals("DSA"))
+			throw new UnsupportedOperationException();
+		out.write(signature);
+	}
+
+	public void writeMysteriousX(SignatureX x) throws IOException {
+		writePublicKey(x.longTermPublicKey);
+		writeInt(x.dhKeyID);
+		writeSignature(x.signature, x.longTermPublicKey);
+	}
+
+	public void writeMysteriousX(SignatureM m) throws IOException {
+		writeBigInt(m.localPubKey.getY());
+		writeBigInt(m.remotePubKey.getY());
+		writePublicKey(m.localLongTermPubKey);
+		writeInt(m.keyPairID);
+	}
+
+	public void writeMysteriousT(MysteriousT t) throws IOException {
+		writeShort(t.protocolVersion);
+		writeByte(t.messageType);
+		writeByte(t.flags);
+
+		writeInt(t.senderKeyID);
+		writeInt(t.recipientKeyID);
+		writeDHPublicKey(t.nextDH);
+		writeCtr(t.ctr);
+		writeData(t.encryptedMessage);
+
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/SerializationConstants.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,29 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io;
+
+/**
+ * 
+ * @author George Politis
+ */
+public interface SerializationConstants {
+
+	public static final String HEAD = "?OTR";
+	public static final char HEAD_ENCODED = ':';
+	public static final char HEAD_ERROR = ' ';
+	public static final char HEAD_QUERY_Q = '?';
+	public static final char HEAD_QUERY_V = 'v';
+
+	public static final int TYPE_LEN_BYTE = 1;
+	public static final int TYPE_LEN_SHORT = 2;
+	public static final int TYPE_LEN_INT = 4;
+	public static final int TYPE_LEN_MAC = 20;
+	public static final int TYPE_LEN_CTR = 8;
+
+	public static final int DATA_LEN = TYPE_LEN_INT;
+	public static final int TLV_LEN = TYPE_LEN_SHORT;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/SerializationUtils.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,341 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.math.BigInteger;
+import java.security.PublicKey;
+import java.util.List;
+import java.util.Vector;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import net.java.otr4j.io.messages.AbstractEncodedMessage;
+import net.java.otr4j.io.messages.AbstractMessage;
+import net.java.otr4j.io.messages.DHCommitMessage;
+import net.java.otr4j.io.messages.DHKeyMessage;
+import net.java.otr4j.io.messages.DataMessage;
+import net.java.otr4j.io.messages.ErrorMessage;
+import net.java.otr4j.io.messages.MysteriousT;
+import net.java.otr4j.io.messages.PlainTextMessage;
+import net.java.otr4j.io.messages.QueryMessage;
+import net.java.otr4j.io.messages.RevealSignatureMessage;
+import net.java.otr4j.io.messages.SignatureM;
+import net.java.otr4j.io.messages.SignatureMessage;
+import net.java.otr4j.io.messages.SignatureX;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class SerializationUtils {
+	// Mysterious X IO.
+	public static SignatureX toMysteriousX(byte[] b) throws IOException {
+		ByteArrayInputStream in = new ByteArrayInputStream(b);
+		OtrInputStream ois = new OtrInputStream(in);
+		SignatureX x = ois.readMysteriousX();
+		ois.close();
+		return x;
+	}
+
+	public static byte[] toByteArray(SignatureX x) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writeMysteriousX(x);
+		byte[] b = out.toByteArray();
+		oos.close();
+		return b;
+	}
+
+	// Mysterious M IO.
+	public static byte[] toByteArray(SignatureM m) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writeMysteriousX(m);
+		byte[] b = out.toByteArray();
+		oos.close();
+		return b;
+	}
+
+	// Mysterious T IO.
+	public static byte[] toByteArray(MysteriousT t) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writeMysteriousT(t);
+		byte[] b = out.toByteArray();
+		out.close();
+		return b;
+	}
+
+	// Basic IO.
+	public static byte[] writeData(byte[] b) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writeData(b);
+		byte[] otrb = out.toByteArray();
+		out.close();
+		return otrb;
+	}
+
+	// BigInteger IO.
+	public static byte[] writeMpi(BigInteger bigInt) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writeBigInt(bigInt);
+		byte[] b = out.toByteArray();
+		oos.close();
+		return b;
+	}
+
+	public static BigInteger readMpi(byte[] b) throws IOException {
+		ByteArrayInputStream in = new ByteArrayInputStream(b);
+		OtrInputStream ois = new OtrInputStream(in);
+		BigInteger bigint = ois.readBigInt();
+		ois.close();
+		return bigint;
+	}
+
+	// Public Key IO.
+	public static byte[] writePublicKey(PublicKey pubKey) throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		OtrOutputStream oos = new OtrOutputStream(out);
+		oos.writePublicKey(pubKey);
+		byte[] b = out.toByteArray();
+		oos.close();
+		return b;
+	}
+
+	// Message IO.
+	public static String toString(AbstractMessage m) throws IOException {
+		StringWriter writer = new StringWriter();
+		writer.write(SerializationConstants.HEAD);
+
+		switch (m.messageType) {
+		case AbstractMessage.MESSAGE_ERROR:
+			ErrorMessage error = (ErrorMessage) m;
+			writer.write(SerializationConstants.HEAD_ERROR);
+			writer.write(error.error);
+			break;
+		case AbstractMessage.MESSAGE_PLAINTEXT:
+			PlainTextMessage plaintxt = (PlainTextMessage) m;
+			writer.write(plaintxt.cleanText);
+			if (plaintxt.versions != null && plaintxt.versions.size() > 0) {
+				writer.write(" \\t  \\t\\t\\t\\t \\t \\t \\t  ");
+				for (int version : plaintxt.versions) {
+					if (version == 1)
+						writer.write("  \\t\\t  \\t ");
+
+					if (version == 2)
+						writer.write(" \\t \\t  \\t ");
+				}
+			}
+			break;
+		case AbstractMessage.MESSAGE_QUERY:
+			QueryMessage query = (QueryMessage) m;
+			if (query.versions.size() == 1 && query.versions.get(0) == 1) {
+				writer.write(SerializationConstants.HEAD_QUERY_Q);
+			} else {
+				writer.write(SerializationConstants.HEAD_QUERY_V);
+				for (int version : query.versions)
+					writer.write(String.valueOf(version));
+
+				writer.write(SerializationConstants.HEAD_QUERY_Q);
+			}
+			break;
+		case AbstractEncodedMessage.MESSAGE_DHKEY:
+		case AbstractEncodedMessage.MESSAGE_REVEALSIG:
+		case AbstractEncodedMessage.MESSAGE_SIGNATURE:
+		case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
+		case AbstractEncodedMessage.MESSAGE_DATA:
+			ByteArrayOutputStream o = new ByteArrayOutputStream();
+			OtrOutputStream s = new OtrOutputStream(o);
+
+			switch (m.messageType) {
+			case AbstractEncodedMessage.MESSAGE_DHKEY:
+				DHKeyMessage dhkey = (DHKeyMessage) m;
+				s.writeShort(dhkey.protocolVersion);
+				s.writeByte(dhkey.messageType);
+				s.writeDHPublicKey(dhkey.dhPublicKey);
+				break;
+			case AbstractEncodedMessage.MESSAGE_REVEALSIG:
+				RevealSignatureMessage revealsig = (RevealSignatureMessage) m;
+				s.writeShort(revealsig.protocolVersion);
+				s.writeByte(revealsig.messageType);
+				s.writeData(revealsig.revealedKey);
+				s.writeData(revealsig.xEncrypted);
+				s.writeMac(revealsig.xEncryptedMAC);
+				break;
+			case AbstractEncodedMessage.MESSAGE_SIGNATURE:
+				SignatureMessage sig = (SignatureMessage) m;
+				s.writeShort(sig.protocolVersion);
+				s.writeByte(sig.messageType);
+				s.writeData(sig.xEncrypted);
+				s.writeMac(sig.xEncryptedMAC);
+				break;
+			case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
+				DHCommitMessage dhcommit = (DHCommitMessage) m;
+				s.writeShort(dhcommit.protocolVersion);
+				s.writeByte(dhcommit.messageType);
+				s.writeData(dhcommit.dhPublicKeyEncrypted);
+				s.writeData(dhcommit.dhPublicKeyHash);
+				break;
+			case AbstractEncodedMessage.MESSAGE_DATA:
+				DataMessage data = (DataMessage) m;
+				s.writeShort(data.protocolVersion);
+				s.writeByte(data.messageType);
+				s.writeByte(data.flags);
+				s.writeInt(data.senderKeyID);
+				s.writeInt(data.recipientKeyID);
+				s.writeDHPublicKey(data.nextDH);
+				s.writeCtr(data.ctr);
+				s.writeData(data.encryptedMessage);
+				s.writeMac(data.mac);
+				s.writeData(data.oldMACKeys);
+				break;
+			}
+
+			writer.write(SerializationConstants.HEAD_ENCODED);
+			writer.write(new String(Base64.encode(o.toByteArray())));
+			writer.write(".");
+			break;
+		default:
+			throw new IOException("Illegal message type.");
+		}
+
+		return writer.toString();
+	}
+
+	static final Pattern patternWhitespace = Pattern
+			.compile("( \\t  \\t\\t\\t\\t \\t \\t \\t  )(  \\t\\t  \\t )?( \\t \\t  \\t )?");
+
+	public static AbstractMessage toMessage(String s) throws IOException {
+		if (s == null || s.length() <= 1)
+			return null;
+
+		if (s.indexOf(SerializationConstants.HEAD) != 0
+				|| s.length() <= SerializationConstants.HEAD.length()) {
+			// Try to detect whitespace tag.
+			final Matcher matcher = patternWhitespace.matcher(s);
+
+			boolean v1 = false;
+			boolean v2 = false;
+			while (matcher.find()) {
+				if (!v1 && matcher.start(2) > -1)
+					v1 = true;
+
+				if (!v2 && matcher.start(3) > -1)
+					v2 = true;
+
+				if (v1 && v2)
+					break;
+			}
+
+			String cleanText = matcher.replaceAll("");
+			List<Integer> versions;
+			if (v1 && v2) {
+				versions = new Vector<Integer>(2);
+				versions.add(0, 1);
+				versions.add(0, 2);
+			} else if (v1) {
+				versions = new Vector<Integer>(1);
+				versions.add(0, 1);
+			} else if (v2) {
+				versions = new Vector<Integer>(1);
+				versions.add(2);
+			} else
+				versions = null;
+
+			return new PlainTextMessage(versions, cleanText);
+		} else {
+			char contentType = s.charAt(SerializationConstants.HEAD.length());
+			String content = s
+					.substring(SerializationConstants.HEAD.length() + 1);
+			switch (contentType) {
+			case SerializationConstants.HEAD_ENCODED:
+				ByteArrayInputStream bin = new ByteArrayInputStream(Base64
+						.decode(content.getBytes()));
+				OtrInputStream otr = new OtrInputStream(bin);
+				// We have an encoded message.
+				int protocolVersion = otr.readShort();
+				int messageType = otr.readByte();
+				switch (messageType) {
+				case AbstractEncodedMessage.MESSAGE_DATA:
+					int flags = otr.readByte();
+					int senderKeyID = otr.readInt();
+					int recipientKeyID = otr.readInt();
+					DHPublicKey nextDH = otr.readDHPublicKey();
+					byte[] ctr = otr.readCtr();
+					byte[] encryptedMessage = otr.readData();
+					byte[] mac = otr.readMac();
+					byte[] oldMacKeys = otr.readMac();
+					return new DataMessage(protocolVersion, flags, senderKeyID,
+							recipientKeyID, nextDH, ctr, encryptedMessage, mac,
+							oldMacKeys);
+				case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
+					byte[] dhPublicKeyEncrypted = otr.readData();
+					byte[] dhPublicKeyHash = otr.readData();
+					return new DHCommitMessage(protocolVersion,
+							dhPublicKeyHash, dhPublicKeyEncrypted);
+				case AbstractEncodedMessage.MESSAGE_DHKEY:
+					DHPublicKey dhPublicKey = otr.readDHPublicKey();
+					return new DHKeyMessage(protocolVersion, dhPublicKey);
+				case AbstractEncodedMessage.MESSAGE_REVEALSIG: {
+					byte[] revealedKey = otr.readData();
+					byte[] xEncrypted = otr.readData();
+					byte[] xEncryptedMac = otr.readMac();
+					return new RevealSignatureMessage(protocolVersion,
+							xEncrypted, xEncryptedMac, revealedKey);
+				}
+				case AbstractEncodedMessage.MESSAGE_SIGNATURE: {
+					byte[] xEncryted = otr.readData();
+					byte[] xEncryptedMac = otr.readMac();
+					return new SignatureMessage(protocolVersion, xEncryted,
+							xEncryptedMac);
+				}
+				default:
+					throw new IOException("Illegal message type.");
+				}
+			case SerializationConstants.HEAD_ERROR:
+				return new ErrorMessage(AbstractMessage.MESSAGE_ERROR, content);
+			case SerializationConstants.HEAD_QUERY_V:
+			case SerializationConstants.HEAD_QUERY_Q:
+				List<Integer> versions = new Vector<Integer>();
+				String versionString = null;
+				if (SerializationConstants.HEAD_QUERY_Q == contentType) {
+					versions.add(1);
+					if (content.charAt(0) == 'v') {
+						versionString = content.substring(1, content
+								.indexOf('?'));
+					}
+				} else if (SerializationConstants.HEAD_QUERY_V == contentType) {
+					versionString = content.substring(0, content.indexOf('?'));
+				}
+
+				if (versionString != null) {
+					StringReader sr = new StringReader(versionString);
+					int c;
+					while ((c = sr.read()) != -1)
+						if (!versions.contains(c))
+							versions.add(Integer.parseInt(String
+									.valueOf((char) c)));
+				}
+				QueryMessage query = new QueryMessage(versions);
+				return query;
+			default:
+				throw new IOException("Uknown message type.");
+			}
+		}
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/AbstractEncodedMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,52 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+/**
+ * 
+ * @author George Politis
+ */
+public abstract class AbstractEncodedMessage extends AbstractMessage {
+	// Fields.
+	public int protocolVersion;
+
+	// Ctor.
+	public AbstractEncodedMessage(int messageType, int protocolVersion) {
+		super(messageType);
+		this.protocolVersion = protocolVersion;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + protocolVersion;
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		AbstractEncodedMessage other = (AbstractEncodedMessage) obj;
+		if (protocolVersion != other.protocolVersion)
+			return false;
+		return true;
+	}
+
+	// Encoded Message Types
+	public static final int MESSAGE_DH_COMMIT = 0x02;
+	public static final int MESSAGE_DATA = 0x03;
+	public static final int MESSAGE_DHKEY = 0x0a;
+	public static final int MESSAGE_REVEALSIG = 0x11;
+	public static final int MESSAGE_SIGNATURE = 0x12;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/AbstractMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,49 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+/**
+ * 
+ * @author George Politis
+ */
+public abstract class AbstractMessage {
+	// Fields.
+	public int messageType;
+
+	// Ctor.
+	public AbstractMessage(int messageType) {
+		this.messageType = messageType;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + messageType;
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		AbstractMessage other = (AbstractMessage) obj;
+		if (messageType != other.messageType)
+			return false;
+		return true;
+	}
+
+	// Unencoded
+	public static final int MESSAGE_ERROR = 0xff;
+	public static final int MESSAGE_QUERY = 0x100;
+	public static final int MESSAGE_PLAINTEXT = 0x102;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/DHCommitMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,55 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.util.Arrays;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class DHCommitMessage extends AbstractEncodedMessage {
+
+	// Fields.
+	public byte[] dhPublicKeyEncrypted;
+	public byte[] dhPublicKeyHash;
+
+	// Ctor.
+	public DHCommitMessage(int protocolVersion, byte[] dhPublicKeyHash,
+			byte[] dhPublicKeyEncrypted) {
+		super(MESSAGE_DH_COMMIT, protocolVersion);
+		this.dhPublicKeyEncrypted = dhPublicKeyEncrypted;
+		this.dhPublicKeyHash = dhPublicKeyHash;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Arrays.hashCode(dhPublicKeyEncrypted);
+		result = prime * result + Arrays.hashCode(dhPublicKeyHash);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		DHCommitMessage other = (DHCommitMessage) obj;
+		if (!Arrays.equals(dhPublicKeyEncrypted, other.dhPublicKeyEncrypted))
+			return false;
+		if (!Arrays.equals(dhPublicKeyHash, other.dhPublicKeyHash))
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/DHKeyMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,53 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class DHKeyMessage extends AbstractEncodedMessage {
+
+	// Fields.
+	public DHPublicKey dhPublicKey;
+
+	// Ctor.
+	public DHKeyMessage(int protocolVersion, DHPublicKey dhPublicKey) {
+		super(MESSAGE_DHKEY, protocolVersion);
+		this.dhPublicKey = dhPublicKey;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		// TODO: Needs work.
+		result = prime * result
+				+ ((dhPublicKey == null) ? 0 : dhPublicKey.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		DHKeyMessage other = (DHKeyMessage) obj;
+		if (dhPublicKey == null) {
+			if (other.dhPublicKey != null)
+				return false;
+		} else if (dhPublicKey.getY().compareTo(other.dhPublicKey.getY()) != 0)
+			return false;
+		return true;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/DataMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,103 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.util.Arrays;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class DataMessage extends AbstractEncodedMessage {
+
+	// Fields.
+	public byte[] mac;
+	public byte[] oldMACKeys;
+
+	public int flags;
+	public int senderKeyID;
+	public int recipientKeyID;
+	public DHPublicKey nextDH;
+	public byte[] ctr;
+	public byte[] encryptedMessage;
+
+	// Ctor.
+	public DataMessage(int protocolVersion, int flags, int senderKeyID,
+			int recipientKeyID, DHPublicKey nextDH, byte[] ctr,
+			byte[] encryptedMessage, byte[] mac, byte[] oldMacKeys) {
+		super(MESSAGE_DATA, protocolVersion);
+
+		this.flags = flags;
+		this.senderKeyID = senderKeyID;
+		this.recipientKeyID = recipientKeyID;
+		this.nextDH = nextDH;
+		this.ctr = ctr;
+		this.encryptedMessage = encryptedMessage;
+		this.mac = mac;
+		this.oldMACKeys = oldMacKeys;
+	}
+
+	public DataMessage(MysteriousT t, byte[] mac, byte[] oldMacKeys) {
+		this(t.protocolVersion, t.flags, t.senderKeyID, t.recipientKeyID,
+				t.nextDH, t.ctr, t.encryptedMessage, mac, oldMacKeys);
+	}
+
+	// Methods.
+	public MysteriousT getT() {
+		return new MysteriousT(protocolVersion, flags, senderKeyID,
+				recipientKeyID, nextDH, ctr, encryptedMessage);
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Arrays.hashCode(ctr);
+		result = prime * result + Arrays.hashCode(encryptedMessage);
+		result = prime * result + flags;
+		result = prime * result + Arrays.hashCode(mac);
+		// TODO: Needs work.
+		result = prime * result + ((nextDH == null) ? 0 : nextDH.hashCode());
+		result = prime * result + Arrays.hashCode(oldMACKeys);
+		result = prime * result + recipientKeyID;
+		result = prime * result + senderKeyID;
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		DataMessage other = (DataMessage) obj;
+		if (!Arrays.equals(ctr, other.ctr))
+			return false;
+		if (!Arrays.equals(encryptedMessage, other.encryptedMessage))
+			return false;
+		if (flags != other.flags)
+			return false;
+		if (!Arrays.equals(mac, other.mac))
+			return false;
+		if (nextDH == null) {
+			if (other.nextDH != null)
+				return false;
+		} else if (!nextDH.equals(other.nextDH))
+			return false;
+		if (!Arrays.equals(oldMACKeys, other.oldMACKeys))
+			return false;
+		if (recipientKeyID != other.recipientKeyID)
+			return false;
+		if (senderKeyID != other.senderKeyID)
+			return false;
+		return true;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/ErrorMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,48 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class ErrorMessage extends AbstractMessage {
+	// Fields.
+	public String error;
+	
+	// Ctor.
+	public ErrorMessage(int messageType, String error) {
+		super(messageType);
+		this.error = error;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + ((error == null) ? 0 : error.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		ErrorMessage other = (ErrorMessage) obj;
+		if (error == null) {
+			if (other.error != null)
+				return false;
+		} else if (!error.equals(other.error))
+			return false;
+		return true;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/MysteriousT.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,82 @@
+package net.java.otr4j.io.messages;
+
+import java.util.Arrays;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+public class MysteriousT {
+	// Fields.
+	public int protocolVersion;
+	public int messageType;
+	public int flags;
+	public int senderKeyID;
+	public int recipientKeyID;
+	public DHPublicKey nextDH;
+	public byte[] ctr;
+	public byte[] encryptedMessage;
+
+	// Ctor.
+	public MysteriousT(int protocolVersion, int flags, int senderKeyID,
+			int recipientKeyID, DHPublicKey nextDH, byte[] ctr,
+			byte[] encryptedMessage) {
+
+		this.protocolVersion = protocolVersion;
+		this.messageType = AbstractEncodedMessage.MESSAGE_DATA;
+		this.flags = flags;
+		this.senderKeyID = senderKeyID;
+		this.recipientKeyID = recipientKeyID;
+		this.nextDH = nextDH;
+		this.ctr = ctr;
+		this.encryptedMessage = encryptedMessage;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		// TODO: Needs work.
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + Arrays.hashCode(ctr);
+		result = prime * result + Arrays.hashCode(encryptedMessage);
+		result = prime * result + flags;
+		result = prime * result + messageType;
+		result = prime * result + ((nextDH == null) ? 0 : nextDH.hashCode());
+		result = prime * result + protocolVersion;
+		result = prime * result + recipientKeyID;
+		result = prime * result + senderKeyID;
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		// TODO: Needs work.
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		MysteriousT other = (MysteriousT) obj;
+		if (!Arrays.equals(ctr, other.ctr))
+			return false;
+		if (!Arrays.equals(encryptedMessage, other.encryptedMessage))
+			return false;
+		if (flags != other.flags)
+			return false;
+		if (messageType != other.messageType)
+			return false;
+		if (nextDH == null) {
+			if (other.nextDH != null)
+				return false;
+		} else if (!nextDH.equals(other.nextDH))
+			return false;
+		if (protocolVersion != other.protocolVersion)
+			return false;
+		if (recipientKeyID != other.recipientKeyID)
+			return false;
+		if (senderKeyID != other.senderKeyID)
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/PlainTextMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,52 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.util.List;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class PlainTextMessage extends QueryMessage {
+	// Fields.
+	public String cleanText;
+
+	// Ctor.
+	public PlainTextMessage(List<Integer> versions, String cleanText) {
+		super(MESSAGE_PLAINTEXT, versions);
+		this.cleanText = cleanText;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result
+				+ ((cleanText == null) ? 0 : cleanText.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		PlainTextMessage other = (PlainTextMessage) obj;
+		if (cleanText == null) {
+			if (other.cleanText != null)
+				return false;
+		} else if (!cleanText.equals(other.cleanText))
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/QueryMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,56 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.util.List;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class QueryMessage extends AbstractMessage {
+	// Fields.
+	public List<Integer> versions;
+
+	// Ctor.
+	protected QueryMessage(int messageType, List<Integer> versions) {
+		super(messageType);
+		this.versions = versions;
+	}
+
+	public QueryMessage(List<Integer> versions) {
+		this(MESSAGE_QUERY, versions);
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result
+				+ ((versions == null) ? 0 : versions.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		QueryMessage other = (QueryMessage) obj;
+		if (versions == null) {
+			if (other.versions != null)
+				return false;
+		} else if (!versions.equals(other.versions))
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/RevealSignatureMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,49 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.util.Arrays;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class RevealSignatureMessage extends SignatureMessage {
+	// Fields.
+	public byte[] revealedKey;
+
+	// Ctor.
+	public RevealSignatureMessage(int protocolVersion, byte[] xEncrypted,
+			byte[] xEncryptedMAC, byte[] revealedKey) {
+		super(MESSAGE_REVEALSIG, protocolVersion, xEncrypted, xEncryptedMAC);
+
+		this.revealedKey = revealedKey;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Arrays.hashCode(revealedKey);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		RevealSignatureMessage other = (RevealSignatureMessage) obj;
+		if (!Arrays.equals(revealedKey, other.revealedKey))
+			return false;
+		return true;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/SignatureM.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,82 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.security.PublicKey;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class SignatureM {
+	// Fields.
+	public DHPublicKey localPubKey;
+	public DHPublicKey remotePubKey;
+	public PublicKey localLongTermPubKey;
+	public int keyPairID;
+	
+	// Ctor.
+	public SignatureM(DHPublicKey localPubKey, DHPublicKey remotePublicKey,
+			PublicKey localLongTermPublicKey, int keyPairID) {
+
+		this.localPubKey = localPubKey;
+		this.remotePubKey = remotePublicKey;
+		this.localLongTermPubKey = localLongTermPublicKey;
+		this.keyPairID = keyPairID;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + keyPairID;
+		// TODO: Needs work.
+		result = prime
+				* result
+				+ ((localLongTermPubKey == null) ? 0 : localLongTermPubKey
+						.hashCode());
+		result = prime * result
+				+ ((localPubKey == null) ? 0 : localPubKey.hashCode());
+		result = prime * result
+				+ ((remotePubKey == null) ? 0 : remotePubKey.hashCode());
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		// TODO: Needs work.
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		SignatureM other = (SignatureM) obj;
+		if (keyPairID != other.keyPairID)
+			return false;
+		if (localLongTermPubKey == null) {
+			if (other.localLongTermPubKey != null)
+				return false;
+		} else if (!localLongTermPubKey.equals(other.localLongTermPubKey))
+			return false;
+		if (localPubKey == null) {
+			if (other.localPubKey != null)
+				return false;
+		} else if (!localPubKey.equals(other.localPubKey))
+			return false;
+		if (remotePubKey == null) {
+			if (other.remotePubKey != null)
+				return false;
+		} else if (!remotePubKey.equals(other.remotePubKey))
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/SignatureMessage.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,82 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.io.SerializationUtils;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class SignatureMessage extends AbstractEncodedMessage {
+	// Fields.
+	public byte[] xEncrypted;
+	public byte[] xEncryptedMAC;
+
+	// Ctor.
+	protected SignatureMessage(int messageType, int protocolVersion,
+			byte[] xEncrypted, byte[] xEncryptedMAC) {
+		super(messageType, protocolVersion);
+		this.xEncrypted = xEncrypted;
+		this.xEncryptedMAC = xEncryptedMAC;
+	}
+
+	public SignatureMessage(int protocolVersion, byte[] xEncrypted,
+			byte[] xEncryptedMAC) {
+		this(MESSAGE_SIGNATURE, protocolVersion, xEncrypted, xEncryptedMAC);
+	}
+
+	// Memthods.
+	public byte[] decrypt(byte[] key) throws OtrException {
+		return new OtrCryptoEngineImpl().aesDecrypt(key, null, xEncrypted);
+	}
+
+	public boolean verify(byte[] key) throws OtrException {
+		// Hash the key.
+		byte[] xbEncrypted;
+		try {
+			xbEncrypted = SerializationUtils.writeData(xEncrypted);
+		} catch (IOException e) {
+			throw new OtrException(e);
+		}
+
+		byte[] xEncryptedMAC = new OtrCryptoEngineImpl().sha256Hmac160(
+				xbEncrypted, key);
+		// Verify signature.
+		return Arrays.equals(xEncryptedMAC, xEncryptedMAC);
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = super.hashCode();
+		result = prime * result + Arrays.hashCode(xEncrypted);
+		result = prime * result + Arrays.hashCode(xEncryptedMAC);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (!super.equals(obj))
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		SignatureMessage other = (SignatureMessage) obj;
+		if (!Arrays.equals(xEncrypted, other.xEncrypted))
+			return false;
+		if (!Arrays.equals(xEncryptedMAC, other.xEncryptedMAC))
+			return false;
+		return true;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/io/messages/SignatureX.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,67 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.io.messages;
+
+import java.security.PublicKey;
+import java.util.Arrays;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class SignatureX {
+	// Fields.
+	public PublicKey longTermPublicKey;
+	public int dhKeyID;
+	public byte[] signature;
+
+	// Ctor.
+	public SignatureX(PublicKey ourLongTermPublicKey, int ourKeyID,
+			byte[] signature) {
+		this.longTermPublicKey = ourLongTermPublicKey;
+		this.dhKeyID = ourKeyID;
+		this.signature = signature;
+	}
+
+	// Methods.
+	@Override
+	public int hashCode() {
+		// TODO: Needs work.
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + dhKeyID;
+		result = prime
+				* result
+				+ ((longTermPublicKey == null) ? 0 : longTermPublicKey
+						.hashCode());
+		result = prime * result + Arrays.hashCode(signature);
+		return result;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		// TODO: Needs work.
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		SignatureX other = (SignatureX) obj;
+		if (dhKeyID != other.dhKeyID)
+			return false;
+		if (longTermPublicKey == null) {
+			if (other.longTermPublicKey != null)
+				return false;
+		} else if (!longTermPublicKey.equals(other.longTermPublicKey))
+			return false;
+		if (!Arrays.equals(signature, other.signature))
+			return false;
+		return true;
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/AuthContext.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,55 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.session;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.PublicKey;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.io.messages.AbstractMessage;
+
+/**
+ * 
+ * @author George Politis
+ */
+interface AuthContext {
+
+	public static final int NONE = 0;
+	public static final int AWAITING_DHKEY = 1;
+	public static final int AWAITING_REVEALSIG = 2;
+	public static final int AWAITING_SIG = 3;
+	public static final int V1_SETUP = 4;
+	public static final byte C_START = (byte) 0x01;
+	public static final byte M1_START = (byte) 0x02;
+	public static final byte M2_START = (byte) 0x03;
+	public static final byte M1p_START = (byte) 0x04;
+	public static final byte M2p_START = (byte) 0x05;
+
+	public abstract void reset();
+
+	public abstract boolean getIsSecure();
+
+	public abstract DHPublicKey getRemoteDHPublicKey();
+
+	public abstract KeyPair getLocalDHKeyPair() throws OtrException;
+
+	public abstract BigInteger getS() throws OtrException;
+
+	public abstract void handleReceivingMessage(AbstractMessage m)
+			throws OtrException;
+
+	public abstract void startV2Auth() throws OtrException;
+
+	public abstract void respondV2Auth() throws OtrException;
+
+	public abstract PublicKey getRemoteLongTermPublicKey();
+
+	public abstract KeyPair getLocalLongTermKeyPair();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/AuthContextImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,766 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.session;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.Vector;
+import java.util.logging.Logger;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngine;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.io.SerializationUtils;
+import net.java.otr4j.io.messages.DHCommitMessage;
+import net.java.otr4j.io.messages.DHKeyMessage;
+import net.java.otr4j.io.messages.AbstractEncodedMessage;
+import net.java.otr4j.io.messages.AbstractMessage;
+import net.java.otr4j.io.messages.SignatureM;
+import net.java.otr4j.io.messages.SignatureX;
+import net.java.otr4j.io.messages.QueryMessage;
+import net.java.otr4j.io.messages.RevealSignatureMessage;
+import net.java.otr4j.io.messages.SignatureMessage;
+
+/**
+ * 
+ * @author George Politis
+ */
+class AuthContextImpl implements AuthContext {
+
+	public AuthContextImpl(Session session) {
+		this.setSession(session);
+		this.reset();
+	}
+
+	private Session session;
+
+	private int authenticationState;
+	private byte[] r;
+
+	private DHPublicKey remoteDHPublicKey;
+	private byte[] remoteDHPublicKeyEncrypted;
+	private byte[] remoteDHPublicKeyHash;
+
+	private KeyPair localDHKeyPair;
+	private int localDHPrivateKeyID;
+	private byte[] localDHPublicKeyBytes;
+	private byte[] localDHPublicKeyHash;
+	private byte[] localDHPublicKeyEncrypted;
+
+	private BigInteger s;
+	private byte[] c;
+	private byte[] m1;
+	private byte[] m2;
+	private byte[] cp;
+	private byte[] m1p;
+	private byte[] m2p;
+
+	private KeyPair localLongTermKeyPair;
+	private Boolean isSecure = false;
+	private int protocolVersion;
+
+	private int getProtocolVersion() {
+		return this.protocolVersion;
+	}
+
+	private void setProtocolVersion(int protoVersion) {
+		this.protocolVersion = protoVersion;
+	}
+
+	private static Logger logger = Logger.getLogger(AuthContextImpl.class
+			.getName());
+
+	class MessageFactory {
+
+		private QueryMessage getQueryMessage() {
+			Vector<Integer> versions = new Vector<Integer>();
+			versions.add(2);
+			return new QueryMessage(versions);
+		}
+
+		private DHCommitMessage getDHCommitMessage() throws OtrException {
+			return new DHCommitMessage(getProtocolVersion(),
+					getLocalDHPublicKeyHash(), getLocalDHPublicKeyEncrypted());
+		}
+
+		private DHKeyMessage getDHKeyMessage() throws OtrException {
+			return new DHKeyMessage(getProtocolVersion(),
+					(DHPublicKey) getLocalDHKeyPair().getPublic());
+		}
+
+		private RevealSignatureMessage getRevealSignatureMessage()
+				throws OtrException {
+			try {
+				SignatureM m = new SignatureM((DHPublicKey) getLocalDHKeyPair()
+						.getPublic(), getRemoteDHPublicKey(),
+						getLocalLongTermKeyPair().getPublic(),
+						getLocalDHKeyPairID());
+
+				OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+				byte[] mhash = otrCryptoEngine.sha256Hmac(SerializationUtils
+						.toByteArray(m), getM1());
+				byte[] signature = otrCryptoEngine.sign(mhash,
+						getLocalLongTermKeyPair().getPrivate());
+
+				SignatureX mysteriousX = new SignatureX(
+						getLocalLongTermKeyPair().getPublic(),
+						getLocalDHKeyPairID(), signature);
+				byte[] xEncrypted = otrCryptoEngine.aesEncrypt(getC(), null,
+						SerializationUtils.toByteArray(mysteriousX));
+
+				byte[] tmp = SerializationUtils.writeData(xEncrypted);
+
+				byte[] xEncryptedHash = otrCryptoEngine.sha256Hmac160(tmp,
+						getM2());
+				return new RevealSignatureMessage(getProtocolVersion(),
+						xEncrypted, xEncryptedHash, getR());
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+		}
+
+		private SignatureMessage getSignatureMessage() throws OtrException {
+			SignatureM m = new SignatureM((DHPublicKey) getLocalDHKeyPair()
+					.getPublic(), getRemoteDHPublicKey(),
+					getLocalLongTermKeyPair().getPublic(),
+					getLocalDHKeyPairID());
+
+			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+			byte[] mhash;
+			try {
+				mhash = otrCryptoEngine.sha256Hmac(SerializationUtils
+						.toByteArray(m), getM1p());
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			byte[] signature = otrCryptoEngine.sign(mhash,
+					getLocalLongTermKeyPair().getPrivate());
+
+			SignatureX mysteriousX = new SignatureX(getLocalLongTermKeyPair()
+					.getPublic(), getLocalDHKeyPairID(), signature);
+
+			byte[] xEncrypted;
+			try {
+				xEncrypted = otrCryptoEngine.aesEncrypt(getCp(), null,
+						SerializationUtils.toByteArray(mysteriousX));
+				byte[] tmp = SerializationUtils.writeData(xEncrypted);
+				byte[] xEncryptedHash = otrCryptoEngine.sha256Hmac160(tmp,
+						getM2p());
+				return new SignatureMessage(getProtocolVersion(), xEncrypted,
+						xEncryptedHash);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+		}
+	}
+
+	private MessageFactory messageFactory = new MessageFactory();
+
+	public void reset() {
+		logger.finest("Resetting authentication state.");
+		authenticationState = AuthContext.NONE;
+		r = null;
+
+		remoteDHPublicKey = null;
+		remoteDHPublicKeyEncrypted = null;
+		remoteDHPublicKeyHash = null;
+
+		localDHKeyPair = null;
+		localDHPrivateKeyID = 1;
+		localDHPublicKeyBytes = null;
+		localDHPublicKeyHash = null;
+		localDHPublicKeyEncrypted = null;
+
+		s = null;
+		c = m1 = m2 = cp = m1p = m2p = null;
+
+		localLongTermKeyPair = null;
+		protocolVersion = 0;
+		setIsSecure(false);
+	}
+
+	private void setIsSecure(Boolean isSecure) {
+		this.isSecure = isSecure;
+	}
+
+	public boolean getIsSecure() {
+		return isSecure;
+	}
+
+	private void setAuthenticationState(int authenticationState) {
+		this.authenticationState = authenticationState;
+	}
+
+	private int getAuthenticationState() {
+		return authenticationState;
+	}
+
+	private byte[] getR() {
+		if (r == null) {
+			logger.finest("Picking random key r.");
+			r = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH];
+			new Random().nextBytes(r);
+		}
+		return r;
+	}
+
+	private void setRemoteDHPublicKey(DHPublicKey dhPublicKey) {
+		// Verifies that Alice's gy is a legal value (2 <= gy <= modulus-2)
+		if (dhPublicKey.getY().compareTo(OtrCryptoEngine.MODULUS_MINUS_TWO) > 0) {
+			throw new IllegalArgumentException(
+					"Illegal D-H Public Key value, Ignoring message.");
+		} else if (dhPublicKey.getY().compareTo(OtrCryptoEngine.BIGINTEGER_TWO) < 0) {
+			throw new IllegalArgumentException(
+					"Illegal D-H Public Key value, Ignoring message.");
+		}
+		logger.finest("Received D-H Public Key is a legal value.");
+
+		this.remoteDHPublicKey = dhPublicKey;
+	}
+
+	public DHPublicKey getRemoteDHPublicKey() {
+		return remoteDHPublicKey;
+	}
+
+	private void setRemoteDHPublicKeyEncrypted(byte[] remoteDHPublicKeyEncrypted) {
+		logger.finest("Storing encrypted remote public key.");
+		this.remoteDHPublicKeyEncrypted = remoteDHPublicKeyEncrypted;
+	}
+
+	private byte[] getRemoteDHPublicKeyEncrypted() {
+		return remoteDHPublicKeyEncrypted;
+	}
+
+	private void setRemoteDHPublicKeyHash(byte[] remoteDHPublicKeyHash) {
+		logger.finest("Storing encrypted remote public key hash.");
+		this.remoteDHPublicKeyHash = remoteDHPublicKeyHash;
+	}
+
+	private byte[] getRemoteDHPublicKeyHash() {
+		return remoteDHPublicKeyHash;
+	}
+
+	public KeyPair getLocalDHKeyPair() throws OtrException {
+		if (localDHKeyPair == null) {
+			localDHKeyPair = new OtrCryptoEngineImpl().generateDHKeyPair();
+			logger.finest("Generated local D-H key pair.");
+		}
+		return localDHKeyPair;
+	}
+
+	private int getLocalDHKeyPairID() {
+		return localDHPrivateKeyID;
+	}
+
+	private byte[] getLocalDHPublicKeyHash() throws OtrException {
+		if (localDHPublicKeyHash == null) {
+			localDHPublicKeyHash = new OtrCryptoEngineImpl()
+					.sha256Hash(getLocalDHPublicKeyBytes());
+			logger.finest("Hashed local D-H public key.");
+		}
+		return localDHPublicKeyHash;
+	}
+
+	private byte[] getLocalDHPublicKeyEncrypted() throws OtrException {
+		if (localDHPublicKeyEncrypted == null) {
+			localDHPublicKeyEncrypted = new OtrCryptoEngineImpl().aesEncrypt(
+					getR(), null, getLocalDHPublicKeyBytes());
+			logger.finest("Encrypted our D-H public key.");
+		}
+		return localDHPublicKeyEncrypted;
+	}
+
+	public BigInteger getS() throws OtrException {
+		if (s == null) {
+			s = new OtrCryptoEngineImpl().generateSecret(this
+					.getLocalDHKeyPair().getPrivate(), this
+					.getRemoteDHPublicKey());
+			logger.finest("Generated shared secret.");
+		}
+		return s;
+	}
+
+	private byte[] getC() throws OtrException {
+		if (c != null)
+			return c;
+
+		byte[] h2 = h2(C_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		this.c = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH];
+		buff.get(this.c);
+		logger.finest("Computed c.");
+		return c;
+
+	}
+
+	private byte[] getM1() throws OtrException {
+		if (m1 != null)
+			return m1;
+
+		byte[] h2 = h2(M1_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		byte[] m1 = new byte[OtrCryptoEngine.SHA256_HMAC_KEY_BYTE_LENGTH];
+		buff.get(m1);
+		logger.finest("Computed m1.");
+		this.m1 = m1;
+		return m1;
+	}
+
+	private byte[] getM2() throws OtrException {
+		if (m2 != null)
+			return m2;
+
+		byte[] h2 = h2(M2_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		byte[] m2 = new byte[OtrCryptoEngine.SHA256_HMAC_KEY_BYTE_LENGTH];
+		buff.get(m2);
+		logger.finest("Computed m2.");
+		this.m2 = m2;
+		return m2;
+	}
+
+	private byte[] getCp() throws OtrException {
+		if (cp != null)
+			return cp;
+
+		byte[] h2 = h2(C_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		byte[] cp = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH];
+		buff.position(OtrCryptoEngine.AES_KEY_BYTE_LENGTH);
+		buff.get(cp);
+		logger.finest("Computed c'.");
+		this.cp = cp;
+		return cp;
+	}
+
+	private byte[] getM1p() throws OtrException {
+		if (m1p != null)
+			return m1p;
+
+		byte[] h2 = h2(M1p_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		byte[] m1p = new byte[OtrCryptoEngine.SHA256_HMAC_KEY_BYTE_LENGTH];
+		buff.get(m1p);
+		this.m1p = m1p;
+		logger.finest("Computed m1'.");
+		return m1p;
+	}
+
+	private byte[] getM2p() throws OtrException {
+		if (m2p != null)
+			return m2p;
+
+		byte[] h2 = h2(M2p_START);
+		ByteBuffer buff = ByteBuffer.wrap(h2);
+		byte[] m2p = new byte[OtrCryptoEngine.SHA256_HMAC_KEY_BYTE_LENGTH];
+		buff.get(m2p);
+		this.m2p = m2p;
+		logger.finest("Computed m2'.");
+		return m2p;
+	}
+
+	public KeyPair getLocalLongTermKeyPair() {
+		if (localLongTermKeyPair == null) {
+			localLongTermKeyPair = getSession().getLocalKeyPair();
+		}
+		return localLongTermKeyPair;
+	}
+
+	private byte[] h2(byte b) throws OtrException {
+		byte[] secbytes;
+		try {
+			secbytes = SerializationUtils.writeMpi(getS());
+		} catch (IOException e) {
+			throw new OtrException(e);
+		}
+
+		int len = secbytes.length + 1;
+		ByteBuffer buff = ByteBuffer.allocate(len);
+		buff.put(b);
+		buff.put(secbytes);
+		byte[] sdata = buff.array();
+		return new OtrCryptoEngineImpl().sha256Hash(sdata);
+	}
+
+	private byte[] getLocalDHPublicKeyBytes() throws OtrException {
+		if (localDHPublicKeyBytes == null) {
+			try {
+				this.localDHPublicKeyBytes = SerializationUtils
+						.writeMpi(((DHPublicKey) getLocalDHKeyPair()
+								.getPublic()).getY());
+
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+		}
+		return localDHPublicKeyBytes;
+	}
+
+	public void handleReceivingMessage(AbstractMessage m) throws OtrException {
+
+		switch (m.messageType) {
+		case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
+			handleDHCommitMessage((DHCommitMessage) m);
+			break;
+		case AbstractEncodedMessage.MESSAGE_DHKEY:
+			handleDHKeyMessage((DHKeyMessage) m);
+			break;
+		case AbstractEncodedMessage.MESSAGE_REVEALSIG:
+			handleRevealSignatureMessage((RevealSignatureMessage) m);
+			break;
+		case AbstractEncodedMessage.MESSAGE_SIGNATURE:
+			handleSignatureMessage((SignatureMessage) m);
+			break;
+		default:
+			throw new UnsupportedOperationException();
+		}
+	}
+
+	private void handleSignatureMessage(SignatureMessage m) throws OtrException {
+		Session session = getSession();
+		SessionID sessionID = session.getSessionID();
+		logger.finest(sessionID.getAccountID()
+				+ " received a signature message from " + sessionID.getUserID()
+				+ " throught " + sessionID.getProtocolName() + ".");
+		if (!session.getSessionPolicy().getAllowV2()) {
+			logger.finest("Policy does not allow OTRv2, ignoring message.");
+			return;
+		}
+
+		switch (this.getAuthenticationState()) {
+		case AWAITING_SIG:
+			// Verify MAC.
+			if (!m.verify(this.getM2p())) {
+				logger
+						.finest("Signature MACs are not equal, ignoring message.");
+				return;
+			}
+
+			// Decrypt X.
+			byte[] remoteXDecrypted = m.decrypt(this.getCp());
+			SignatureX remoteX;
+			try {
+				remoteX = SerializationUtils.toMysteriousX(remoteXDecrypted);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+			// Compute signature.
+			PublicKey remoteLongTermPublicKey = remoteX.longTermPublicKey;
+			SignatureM remoteM = new SignatureM(this.getRemoteDHPublicKey(),
+					(DHPublicKey) this.getLocalDHKeyPair().getPublic(),
+					remoteLongTermPublicKey, remoteX.dhKeyID);
+			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+			// Verify signature.
+			byte[] signature;
+			try {
+				signature = otrCryptoEngine.sha256Hmac(SerializationUtils
+						.toByteArray(remoteM), this.getM1p());
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+			if (!otrCryptoEngine.verify(signature, remoteLongTermPublicKey,
+					remoteX.signature)) {
+				logger.finest("Signature verification failed.");
+				return;
+			}
+
+			this.setIsSecure(true);
+			this.setRemoteLongTermPublicKey(remoteLongTermPublicKey);
+			break;
+		default:
+			logger
+					.finest("We were not expecting a signature, ignoring message.");
+			return;
+		}
+	}
+
+	private void handleRevealSignatureMessage(RevealSignatureMessage m)
+			throws OtrException {
+		Session session = getSession();
+		SessionID sessionID = session.getSessionID();
+		logger.finest(sessionID.getAccountID()
+				+ " received a reveal signature message from "
+				+ sessionID.getUserID() + " throught "
+				+ sessionID.getProtocolName() + ".");
+
+		if (!session.getSessionPolicy().getAllowV2()) {
+			logger.finest("Policy does not allow OTRv2, ignoring message.");
+			return;
+		}
+
+		switch (this.getAuthenticationState()) {
+		case AWAITING_REVEALSIG:
+			// Use the received value of r to decrypt the value of gx
+			// received
+			// in the D-H Commit Message, and verify the hash therein.
+			// Decrypt
+			// the encrypted signature, and verify the signature and the
+			// MACs.
+			// If everything checks out:
+
+			// * Reply with a Signature Message.
+			// * Transition authstate to AUTHSTATE_NONE.
+			// * Transition msgstate to MSGSTATE_ENCRYPTED.
+			// * TODO If there is a recent stored message, encrypt it and
+			// send
+			// it as a Data Message.
+
+			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+			// Uses r to decrypt the value of gx sent earlier
+			byte[] remoteDHPublicKeyDecrypted = otrCryptoEngine.aesDecrypt(
+					m.revealedKey, null, this.getRemoteDHPublicKeyEncrypted());
+
+			// Verifies that HASH(gx) matches the value sent earlier
+			byte[] remoteDHPublicKeyHash = otrCryptoEngine
+					.sha256Hash(remoteDHPublicKeyDecrypted);
+			if (!Arrays.equals(remoteDHPublicKeyHash, this
+					.getRemoteDHPublicKeyHash())) {
+				logger.finest("Hashes don't match, ignoring message.");
+				return;
+			}
+
+			// Verifies that Bob's gx is a legal value (2 <= gx <=
+			// modulus-2)
+			BigInteger remoteDHPublicKeyMpi;
+			try {
+				remoteDHPublicKeyMpi = SerializationUtils
+						.readMpi(remoteDHPublicKeyDecrypted);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			this.setRemoteDHPublicKey(otrCryptoEngine
+					.getDHPublicKey(remoteDHPublicKeyMpi));
+
+			// Verify received Data.
+			if (!m.verify(this.getM2())) {
+				logger
+						.finest("Signature MACs are not equal, ignoring message.");
+				return;
+			}
+
+			// Decrypt X.
+			byte[] remoteXDecrypted = m.decrypt(this.getC());
+			SignatureX remoteX;
+			try {
+				remoteX = SerializationUtils.toMysteriousX(remoteXDecrypted);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			// Compute signature.
+			PublicKey remoteLongTermPublicKey = remoteX.longTermPublicKey;
+			SignatureM remoteM = new SignatureM(this.getRemoteDHPublicKey(),
+					(DHPublicKey) this.getLocalDHKeyPair().getPublic(),
+					remoteLongTermPublicKey, remoteX.dhKeyID);
+
+			// Verify signature.
+			byte[] signature;
+			try {
+				signature = otrCryptoEngine.sha256Hmac(SerializationUtils
+						.toByteArray(remoteM), this.getM1());
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			if (!otrCryptoEngine.verify(signature, remoteLongTermPublicKey,
+					remoteX.signature)) {
+				logger.finest("Signature verification failed.");
+				return;
+			}
+
+			logger.finest("Signature verification succeeded.");
+
+			this.setAuthenticationState(AuthContext.NONE);
+			this.setIsSecure(true);
+			this.setRemoteLongTermPublicKey(remoteLongTermPublicKey);
+			getSession().injectMessage(messageFactory.getSignatureMessage());
+			break;
+		default:
+			logger.finest("Ignoring message.");
+			break;
+		}
+	}
+
+	private void handleDHKeyMessage(DHKeyMessage m) throws OtrException {
+		Session session = getSession();
+		SessionID sessionID = session.getSessionID();
+		logger.finest(sessionID.getAccountID()
+				+ " received a D-H key message from " + sessionID.getUserID()
+				+ " throught " + sessionID.getProtocolName() + ".");
+
+		if (!session.getSessionPolicy().getAllowV2()) {
+			logger.finest("If ALLOW_V2 is not set, ignore this message.");
+			return;
+		}
+
+		switch (this.getAuthenticationState()) {
+		case AWAITING_DHKEY:
+			// Reply with a Reveal Signature Message and transition
+			// authstate to
+			// AUTHSTATE_AWAITING_SIG
+			this.setRemoteDHPublicKey(m.dhPublicKey);
+			this.setAuthenticationState(AuthContext.AWAITING_SIG);
+			getSession().injectMessage(
+					messageFactory.getRevealSignatureMessage());
+			logger.finest("Sent Reveal Signature.");
+			break;
+		case AWAITING_SIG:
+
+			if (m.dhPublicKey.getY().equals(this.getRemoteDHPublicKey().getY())) {
+				// If this D-H Key message is the same the one you received
+				// earlier (when you entered AUTHSTATE_AWAITING_SIG):
+				// Retransmit
+				// your Reveal Signature Message.
+				getSession().injectMessage(
+						messageFactory.getRevealSignatureMessage());
+				logger.finest("Resent Reveal Signature.");
+			} else {
+				// Otherwise: Ignore the message.
+				logger.finest("Ignoring message.");
+			}
+			break;
+		default:
+			// Ignore the message
+			break;
+		}
+	}
+
+	private void handleDHCommitMessage(DHCommitMessage m) throws OtrException {
+		Session session = getSession();
+		SessionID sessionID = session.getSessionID();
+		logger.finest(sessionID.getAccountID()
+				+ " received a D-H commit message from "
+				+ sessionID.getUserID() + " throught "
+				+ sessionID.getProtocolName() + ".");
+
+		if (!session.getSessionPolicy().getAllowV2()) {
+			logger.finest("ALLOW_V2 is not set, ignore this message.");
+			return;
+		}
+
+		switch (this.getAuthenticationState()) {
+		case NONE:
+			// Reply with a D-H Key Message, and transition authstate to
+			// AUTHSTATE_AWAITING_REVEALSIG.
+			this.reset();
+			this.setProtocolVersion(2);
+			this.setRemoteDHPublicKeyEncrypted(m.dhPublicKeyEncrypted);
+			this.setRemoteDHPublicKeyHash(m.dhPublicKeyHash);
+			this.setAuthenticationState(AuthContext.AWAITING_REVEALSIG);
+			getSession().injectMessage(messageFactory.getDHKeyMessage());
+			logger.finest("Sent D-H key.");
+			break;
+
+		case AWAITING_DHKEY:
+			// This is the trickiest transition in the whole protocol. It
+			// indicates that you have already sent a D-H Commit message to
+			// your
+			// correspondent, but that he either didn't receive it, or just
+			// didn't receive it yet, and has sent you one as well. The
+			// symmetry
+			// will be broken by comparing the hashed gx you sent in your
+			// D-H
+			// Commit Message with the one you received, considered as
+			// 32-byte
+			// unsigned big-endian values.
+			BigInteger ourHash = new BigInteger(1, this
+					.getLocalDHPublicKeyHash());
+			BigInteger theirHash = new BigInteger(1, m.dhPublicKeyHash);
+
+			if (theirHash.compareTo(ourHash) == -1) {
+				// Ignore the incoming D-H Commit message, but resend your
+				// D-H
+				// Commit message.
+				getSession().injectMessage(messageFactory.getDHCommitMessage());
+				logger
+						.finest("Ignored the incoming D-H Commit message, but resent our D-H Commit message.");
+			} else {
+				// *Forget* your old gx value that you sent (encrypted)
+				// earlier,
+				// and pretend you're in AUTHSTATE_NONE; i.e. reply with a
+				// D-H
+				// Key Message, and transition authstate to
+				// AUTHSTATE_AWAITING_REVEALSIG.
+				this.reset();
+				this.setProtocolVersion(2);
+				this.setRemoteDHPublicKeyEncrypted(m.dhPublicKeyEncrypted);
+				this.setRemoteDHPublicKeyHash(m.dhPublicKeyHash);
+				this.setAuthenticationState(AuthContext.AWAITING_REVEALSIG);
+				getSession().injectMessage(messageFactory.getDHKeyMessage());
+				logger
+						.finest("Forgot our old gx value that we sent (encrypted) earlier, and pretended we're in AUTHSTATE_NONE -> Sent D-H key.");
+			}
+			break;
+
+		case AWAITING_REVEALSIG:
+			// Retransmit your D-H Key Message (the same one as you sent
+			// when
+			// you entered AUTHSTATE_AWAITING_REVEALSIG). Forget the old D-H
+			// Commit message, and use this new one instead.
+			this.setRemoteDHPublicKeyEncrypted(m.dhPublicKeyEncrypted);
+			this.setRemoteDHPublicKeyHash(m.dhPublicKeyHash);
+			getSession().injectMessage(messageFactory.getDHKeyMessage());
+			logger.finest("Sent D-H key.");
+			break;
+		case AWAITING_SIG:
+			// Reply with a new D-H Key message, and transition authstate to
+			// AUTHSTATE_AWAITING_REVEALSIG
+			this.reset();
+			this.setRemoteDHPublicKeyEncrypted(m.dhPublicKeyEncrypted);
+			this.setRemoteDHPublicKeyHash(m.dhPublicKeyHash);
+			this.setAuthenticationState(AuthContext.AWAITING_REVEALSIG);
+			getSession().injectMessage(messageFactory.getDHKeyMessage());
+			logger.finest("Sent D-H key.");
+			break;
+		case V1_SETUP:
+			throw new UnsupportedOperationException();
+		}
+	}
+
+	public void startV2Auth() throws OtrException {
+		logger
+				.finest("Starting Authenticated Key Exchange, sending query message");
+		getSession().injectMessage(messageFactory.getQueryMessage());
+	}
+
+	public void respondV2Auth() throws OtrException {
+		logger.finest("Responding to Query Message");
+		this.reset();
+		this.setProtocolVersion(2);
+		this.setAuthenticationState(AuthContext.AWAITING_DHKEY);
+		logger.finest("Sending D-H Commit.");
+		getSession().injectMessage(messageFactory.getDHCommitMessage());
+	}
+
+	private void setSession(Session session) {
+		this.session = session;
+	}
+
+	private Session getSession() {
+		return session;
+	}
+
+	private PublicKey remoteLongTermPublicKey;
+
+	public PublicKey getRemoteLongTermPublicKey() {
+		return remoteLongTermPublicKey;
+	}
+
+	private void setRemoteLongTermPublicKey(PublicKey pubKey) {
+		this.remoteLongTermPublicKey = pubKey;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/Session.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,42 @@
+package net.java.otr4j.session;
+
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.List;
+
+import net.java.otr4j.OtrEngineListener;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.io.messages.AbstractMessage;
+import net.java.otr4j.session.SessionImpl.TLV;
+
+public interface Session {
+
+	public abstract SessionStatus getSessionStatus();
+
+	public abstract SessionID getSessionID();
+
+	public abstract void injectMessage(AbstractMessage m) throws OtrException;
+
+	public abstract KeyPair getLocalKeyPair();
+
+	public abstract OtrPolicy getSessionPolicy();
+
+	public abstract String transformReceiving(String content)
+			throws OtrException;
+
+	public abstract String transformSending(String content, List<TLV> tlvs)
+			throws OtrException;
+
+	public abstract void startSession() throws OtrException;
+
+	public abstract void endSession() throws OtrException;
+
+	public abstract void refreshSession() throws OtrException;
+
+	public abstract PublicKey getRemotePublicKey();
+
+	public abstract void addOtrEngineListener(OtrEngineListener l);
+
+	public abstract void removeOtrEngineListener(OtrEngineListener l);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/SessionID.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,70 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.session;
+
+/**
+ * 
+ * @author George Politis
+ * 
+ */
+public final class SessionID {
+
+	public SessionID(String accountID, String userID, String protocolName) {
+		this.setAccountID(accountID);
+		this.setUserID(userID);
+		this.setProtocolName(protocolName);
+	}
+
+	private String accountID;
+	private String userID;
+	private String protocolName;
+	public static final SessionID Empty = new SessionID(null, null, null);
+
+	public void setAccountID(String accountID) {
+		this.accountID = accountID;
+	}
+
+	public String getAccountID() {
+		return accountID;
+	}
+
+	private void setUserID(String userID) {
+		this.userID = userID;
+	}
+
+	public String getUserID() {
+		return userID;
+	}
+
+	private void setProtocolName(String protocolName) {
+		this.protocolName = protocolName;
+	}
+
+	public String getProtocolName() {
+		return protocolName;
+	}
+
+	public String toString() {
+		return this.getAccountID() + "_" + this.getProtocolName() + "_"
+				+ this.getUserID();
+	}
+
+	public boolean equals(Object obj) {
+		if (obj == this)
+			return true;
+		if (obj == null || obj.getClass() != this.getClass())
+			return false;
+
+		SessionID sessionID = (SessionID) obj;
+
+		return this.toString().equals(sessionID.toString());
+	}
+
+	public int hashCode() {
+		return this.toString().hashCode();
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/SessionImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,795 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+
+package net.java.otr4j.session;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Vector;
+import java.util.logging.Logger;
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrEngineListener;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.crypto.OtrCryptoEngine;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.io.OtrInputStream;
+import net.java.otr4j.io.OtrOutputStream;
+import net.java.otr4j.io.SerializationConstants;
+import net.java.otr4j.io.SerializationUtils;
+import net.java.otr4j.io.messages.DataMessage;
+import net.java.otr4j.io.messages.AbstractEncodedMessage;
+import net.java.otr4j.io.messages.ErrorMessage;
+import net.java.otr4j.io.messages.AbstractMessage;
+import net.java.otr4j.io.messages.MysteriousT;
+import net.java.otr4j.io.messages.PlainTextMessage;
+import net.java.otr4j.io.messages.QueryMessage;
+
+/**
+ * 
+ * @author George Politis
+ */
+public class SessionImpl implements Session {
+
+	/**
+	 * 
+	 * @author George Politis
+	 * 
+	 */
+	class TLV {
+		public TLV(int type, byte[] value) {
+			this.setType(type);
+			this.setValue(value);
+		}
+
+		public void setType(int type) {
+			this.type = type;
+		}
+
+		public int getType() {
+			return type;
+		}
+
+		public void setValue(byte[] value) {
+			this.value = value;
+		}
+
+		public byte[] getValue() {
+			return value;
+		}
+
+		private int type;
+		private byte[] value;
+	}
+
+	private SessionID sessionID;
+	private OtrEngineHost listener;
+	private SessionStatus sessionStatus;
+	private AuthContext authContext;
+	private SessionKeys[][] sessionKeys;
+	private Vector<byte[]> oldMacKeys;
+	private static Logger logger = Logger
+			.getLogger(SessionImpl.class.getName());
+
+	public SessionImpl(SessionID sessionID, OtrEngineHost listener) {
+
+		this.setSessionID(sessionID);
+		this.setListener(listener);
+
+		// client application calls OtrEngine.getSessionStatus()
+		// -> create new session if it does not exist, end up here
+		// -> setSessionStatus() fires statusChangedEvent
+		// -> client application calls OtrEngine.getSessionStatus()
+		this.sessionStatus = SessionStatus.PLAINTEXT;
+	}
+
+	private SessionKeys getEncryptionSessionKeys() {
+		logger.finest("Getting encryption keys");
+		return getSessionKeysByIndex(SessionKeys.Previous, SessionKeys.Current);
+	}
+
+	private SessionKeys getMostRecentSessionKeys() {
+		logger.finest("Getting most recent keys.");
+		return getSessionKeysByIndex(SessionKeys.Current, SessionKeys.Current);
+	}
+
+	private SessionKeys getSessionKeysByID(int localKeyID, int remoteKeyID) {
+		logger
+				.finest("Searching for session keys with (localKeyID, remoteKeyID) = ("
+						+ localKeyID + "," + remoteKeyID + ")");
+
+		for (int i = 0; i < getSessionKeys().length; i++) {
+			for (int j = 0; j < getSessionKeys()[i].length; j++) {
+				SessionKeys current = getSessionKeysByIndex(i, j);
+				if (current.getLocalKeyID() == localKeyID
+						&& current.getRemoteKeyID() == remoteKeyID) {
+					logger.finest("Matching keys found.");
+					return current;
+				}
+			}
+		}
+
+		return null;
+	}
+
+	private SessionKeys getSessionKeysByIndex(int localKeyIndex,
+			int remoteKeyIndex) {
+		if (getSessionKeys()[localKeyIndex][remoteKeyIndex] == null)
+			getSessionKeys()[localKeyIndex][remoteKeyIndex] = new SessionKeysImpl(
+					localKeyIndex, remoteKeyIndex);
+
+		return getSessionKeys()[localKeyIndex][remoteKeyIndex];
+	}
+
+	private void rotateRemoteSessionKeys(DHPublicKey pubKey)
+			throws OtrException {
+
+		logger.finest("Rotating remote keys.");
+		SessionKeys sess1 = getSessionKeysByIndex(SessionKeys.Current,
+				SessionKeys.Previous);
+		if (sess1.getIsUsedReceivingMACKey()) {
+			logger
+					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
+			getOldMacKeys().add(sess1.getReceivingMACKey());
+		}
+
+		SessionKeys sess2 = getSessionKeysByIndex(SessionKeys.Previous,
+				SessionKeys.Previous);
+		if (sess2.getIsUsedReceivingMACKey()) {
+			logger
+					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
+			getOldMacKeys().add(sess2.getReceivingMACKey());
+		}
+
+		SessionKeys sess3 = getSessionKeysByIndex(SessionKeys.Current,
+				SessionKeys.Current);
+		sess1
+				.setRemoteDHPublicKey(sess3.getRemoteKey(), sess3
+						.getRemoteKeyID());
+
+		SessionKeys sess4 = getSessionKeysByIndex(SessionKeys.Previous,
+				SessionKeys.Current);
+		sess2
+				.setRemoteDHPublicKey(sess4.getRemoteKey(), sess4
+						.getRemoteKeyID());
+
+		sess3.setRemoteDHPublicKey(pubKey, sess3.getRemoteKeyID() + 1);
+		sess4.setRemoteDHPublicKey(pubKey, sess4.getRemoteKeyID() + 1);
+	}
+
+	private void rotateLocalSessionKeys() throws OtrException {
+
+		logger.finest("Rotating local keys.");
+		SessionKeys sess1 = getSessionKeysByIndex(SessionKeys.Previous,
+				SessionKeys.Current);
+		if (sess1.getIsUsedReceivingMACKey()) {
+			logger
+					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
+			getOldMacKeys().add(sess1.getReceivingMACKey());
+		}
+
+		SessionKeys sess2 = getSessionKeysByIndex(SessionKeys.Previous,
+				SessionKeys.Previous);
+		if (sess2.getIsUsedReceivingMACKey()) {
+			logger
+					.finest("Detected used Receiving MAC key. Adding to old MAC keys to reveal it.");
+			getOldMacKeys().add(sess2.getReceivingMACKey());
+		}
+
+		SessionKeys sess3 = getSessionKeysByIndex(SessionKeys.Current,
+				SessionKeys.Current);
+		sess1.setLocalPair(sess3.getLocalPair(), sess3.getLocalKeyID());
+		SessionKeys sess4 = getSessionKeysByIndex(SessionKeys.Current,
+				SessionKeys.Previous);
+		sess2.setLocalPair(sess4.getLocalPair(), sess4.getLocalKeyID());
+
+		KeyPair newPair = new OtrCryptoEngineImpl().generateDHKeyPair();
+		sess3.setLocalPair(newPair, sess3.getLocalKeyID() + 1);
+		sess4.setLocalPair(newPair, sess4.getLocalKeyID() + 1);
+	}
+
+	private byte[] collectOldMacKeys() {
+		logger.finest("Collecting old MAC keys to be revealed.");
+		int len = 0;
+		for (int i = 0; i < getOldMacKeys().size(); i++)
+			len += getOldMacKeys().get(i).length;
+
+		ByteBuffer buff = ByteBuffer.allocate(len);
+		for (int i = 0; i < getOldMacKeys().size(); i++)
+			buff.put(getOldMacKeys().get(i));
+
+		getOldMacKeys().clear();
+		return buff.array();
+	}
+
+	private void setSessionStatus(SessionStatus sessionStatus)
+			throws OtrException {
+
+		if (sessionStatus == this.sessionStatus)
+			return;
+
+		switch (sessionStatus) {
+		case ENCRYPTED:
+			AuthContext auth = this.getAuthContext();
+			logger.finest("Setting most recent session keys from auth.");
+			for (int i = 0; i < this.getSessionKeys()[0].length; i++) {
+				SessionKeys current = getSessionKeysByIndex(0, i);
+				current.setLocalPair(auth.getLocalDHKeyPair(), 1);
+				current.setRemoteDHPublicKey(auth.getRemoteDHPublicKey(), 1);
+				current.setS(auth.getS());
+			}
+
+			KeyPair nextDH = new OtrCryptoEngineImpl().generateDHKeyPair();
+			for (int i = 0; i < this.getSessionKeys()[1].length; i++) {
+				SessionKeys current = getSessionKeysByIndex(1, i);
+				current.setRemoteDHPublicKey(auth.getRemoteDHPublicKey(), 1);
+				current.setLocalPair(nextDH, 2);
+			}
+
+			this.setRemotePublicKey(auth.getRemoteLongTermPublicKey());
+
+			auth.reset();
+			break;
+		}
+
+		this.sessionStatus = sessionStatus;
+
+		for (OtrEngineListener l : this.listeners)
+			l.sessionStatusChanged(getSessionID());
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.java.otr4j.session.ISession#getSessionStatus()
+	 */
+
+	public SessionStatus getSessionStatus() {
+		return sessionStatus;
+	}
+
+	private void setSessionID(SessionID sessionID) {
+		this.sessionID = sessionID;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.java.otr4j.session.ISession#getSessionID()
+	 */
+	public SessionID getSessionID() {
+		return sessionID;
+	}
+
+	private void setListener(OtrEngineHost listener) {
+		this.listener = listener;
+	}
+
+	private OtrEngineHost getListener() {
+		return listener;
+	}
+
+	private SessionKeys[][] getSessionKeys() {
+		if (sessionKeys == null)
+			sessionKeys = new SessionKeys[2][2];
+		return sessionKeys;
+	}
+
+	private AuthContext getAuthContext() {
+		if (authContext == null)
+			authContext = new AuthContextImpl(this);
+		return authContext;
+	}
+
+	private Vector<byte[]> getOldMacKeys() {
+		if (oldMacKeys == null)
+			oldMacKeys = new Vector<byte[]>();
+		return oldMacKeys;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * net.java.otr4j.session.ISession#handleReceivingMessage(java.lang.String)
+	 */
+	public String transformReceiving(String msgText) throws OtrException {
+		OtrPolicy policy = getSessionPolicy();
+		if (!policy.getAllowV1() && !policy.getAllowV2()) {
+			logger
+					.finest("Policy does not allow neither V1 not V2, ignoring message.");
+			return msgText;
+		}
+
+		AbstractMessage m;
+		try {
+			m = SerializationUtils.toMessage(msgText);
+		} catch (IOException e) {
+			throw new OtrException(e);
+		}
+
+		switch (m.messageType) {
+		case AbstractEncodedMessage.MESSAGE_DATA:
+			return handleDataMessage((DataMessage) m);
+		case AbstractMessage.MESSAGE_ERROR:
+			handleErrorMessage((ErrorMessage) m);
+			return null;
+		case AbstractMessage.MESSAGE_PLAINTEXT:
+			return handlePlainTextMessage((PlainTextMessage) m);
+		case AbstractMessage.MESSAGE_QUERY:
+			handleQueryMessage((QueryMessage) m);
+			return null;
+		case AbstractEncodedMessage.MESSAGE_DH_COMMIT:
+		case AbstractEncodedMessage.MESSAGE_DHKEY:
+		case AbstractEncodedMessage.MESSAGE_REVEALSIG:
+		case AbstractEncodedMessage.MESSAGE_SIGNATURE:
+			AuthContext auth = this.getAuthContext();
+			auth.handleReceivingMessage(m);
+
+			if (auth.getIsSecure()) {
+				this.setSessionStatus(SessionStatus.ENCRYPTED);
+				logger.finest("Gone Secure.");
+			}
+			return null;
+		default:
+			throw new UnsupportedOperationException(
+					"Received an uknown message type.");
+		}
+	}
+
+	private void handleQueryMessage(QueryMessage queryMessage)
+			throws OtrException {
+		logger.finest(getSessionID().getAccountID()
+				+ " received a query message from "
+				+ getSessionID().getUserID() + " throught "
+				+ getSessionID().getProtocolName() + ".");
+
+		setSessionStatus(SessionStatus.PLAINTEXT);
+
+		OtrPolicy policy = getSessionPolicy();
+		if (queryMessage.versions.contains(2) && policy.getAllowV2()) {
+			logger.finest("Query message with V2 support found.");
+			getAuthContext().respondV2Auth();
+		} else if (queryMessage.versions.contains(1) && policy.getAllowV1()) {
+			throw new UnsupportedOperationException();
+		}
+	}
+
+	private void handleErrorMessage(ErrorMessage errorMessage)
+			throws OtrException {
+		logger.finest(getSessionID().getAccountID()
+				+ " received an error message from "
+				+ getSessionID().getUserID() + " throught "
+				+ getSessionID().getUserID() + ".");
+
+		getListener().showError(this.getSessionID(), errorMessage.error);
+
+		OtrPolicy policy = getSessionPolicy();
+		if (policy.getErrorStartAKE()) {
+			logger.finest("Error message starts AKE.");
+			Vector<Integer> versions = new Vector<Integer>();
+			if (policy.getAllowV1())
+				versions.add(1);
+
+			if (policy.getAllowV2())
+				versions.add(2);
+
+			logger.finest("Sending Query");
+			injectMessage(new QueryMessage(versions));
+		}
+	}
+
+	private String handleDataMessage(DataMessage data) throws OtrException {
+		logger.finest(getSessionID().getAccountID()
+				+ " received a data message from " + getSessionID().getUserID()
+				+ ".");
+
+		switch (this.getSessionStatus()) {
+		case ENCRYPTED:
+			logger
+					.finest("Message state is ENCRYPTED. Trying to decrypt message.");
+
+			// Find matching session keys.
+			int senderKeyID = data.senderKeyID;
+			int receipientKeyID = data.recipientKeyID;
+			SessionKeys matchingKeys = this.getSessionKeysByID(receipientKeyID,
+					senderKeyID);
+
+			if (matchingKeys == null) {
+				logger.finest("No matching keys found.");
+				return null;
+			}
+
+			// Verify received MAC with a locally calculated MAC.
+			logger
+					.finest("Transforming T to byte[] to calculate it's HmacSHA1.");
+
+			byte[] serializedT;
+			try {
+				serializedT = SerializationUtils.toByteArray(data.getT());
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+
+			byte[] computedMAC = otrCryptoEngine.sha1Hmac(serializedT,
+					matchingKeys.getReceivingMACKey(),
+					SerializationConstants.TYPE_LEN_MAC);
+
+			if (!Arrays.equals(computedMAC, data.mac)) {
+				logger.finest("MAC verification failed, ignoring message");
+				return null;
+			}
+
+			logger.finest("Computed HmacSHA1 value matches sent one.");
+
+			// Mark this MAC key as old to be revealed.
+			matchingKeys.setIsUsedReceivingMACKey(true);
+
+			matchingKeys.setReceivingCtr(data.ctr);
+
+			byte[] dmc = otrCryptoEngine.aesDecrypt(matchingKeys
+					.getReceivingAESKey(), matchingKeys.getReceivingCtr(),
+					data.encryptedMessage);
+			String decryptedMsgContent;
+			try {
+				// Expect bytes to be text encoded in UTF-8.
+				decryptedMsgContent = new String(dmc, "UTF-8");
+			} catch (UnsupportedEncodingException e) {
+				throw new OtrException(e);
+			}
+
+			logger.finest("Decrypted message: \"" + decryptedMsgContent + "\"");
+
+			// Rotate keys if necessary.
+			SessionKeys mostRecent = this.getMostRecentSessionKeys();
+			if (mostRecent.getLocalKeyID() == receipientKeyID)
+				this.rotateLocalSessionKeys();
+
+			if (mostRecent.getRemoteKeyID() == senderKeyID)
+				this.rotateRemoteSessionKeys(data.nextDH);
+
+			// Handle TLVs
+			List<TLV> tlvs = null;
+			int tlvIndex = decryptedMsgContent.indexOf((char) 0x0);
+			if (tlvIndex > -1) {
+				decryptedMsgContent = decryptedMsgContent
+						.substring(0, tlvIndex);
+				tlvIndex++;
+				byte[] tlvsb = new byte[dmc.length - tlvIndex];
+				System.arraycopy(dmc, tlvIndex, tlvsb, 0, tlvsb.length);
+
+				tlvs = new Vector<TLV>();
+				ByteArrayInputStream tin = new ByteArrayInputStream(tlvsb);
+				while (tin.available() > 0) {
+					int type;
+					byte[] tdata;
+					OtrInputStream eois = new OtrInputStream(tin);
+					try {
+						type = eois.readShort();
+						tdata = eois.readTlvData();
+						eois.close();
+					} catch (IOException e) {
+						throw new OtrException(e);
+					}
+
+					tlvs.add(new TLV(type, tdata));
+				}
+			}
+			if (tlvs != null && tlvs.size() > 0) {
+				for (TLV tlv : tlvs) {
+					switch (tlv.getType()) {
+					case 1:
+						this.setSessionStatus(SessionStatus.FINISHED);
+						return null;
+					default:
+						return decryptedMsgContent;
+					}
+				}
+			}
+
+			return decryptedMsgContent;
+
+		case FINISHED:
+		case PLAINTEXT:
+			getListener().showWarning(this.getSessionID(),
+					"Unreadable encrypted message was received.");
+
+			injectMessage(new ErrorMessage(AbstractMessage.MESSAGE_ERROR,
+					"You sent me an unreadable encrypted message.."));
+			break;
+		}
+
+		return null;
+	}
+
+	public void injectMessage(AbstractMessage m) throws OtrException {
+		String msg;
+		try {
+			msg = SerializationUtils.toString(m);
+		} catch (IOException e) {
+			throw new OtrException(e);
+		}
+		getListener().injectMessage(getSessionID(), msg);
+	}
+
+	private String handlePlainTextMessage(PlainTextMessage plainTextMessage)
+			throws OtrException {
+		logger.finest(getSessionID().getAccountID()
+				+ " received a plaintext message from "
+				+ getSessionID().getUserID() + " throught "
+				+ getSessionID().getProtocolName() + ".");
+
+		OtrPolicy policy = getSessionPolicy();
+		List<Integer> versions = plainTextMessage.versions;
+		if (versions == null || versions.size() < 1) {
+			logger
+					.finest("Received plaintext message without the whitespace tag.");
+			switch (this.getSessionStatus()) {
+			case ENCRYPTED:
+			case FINISHED:
+				// Display the message to the user, but warn him that the
+				// message was received unencrypted.
+				getListener().showWarning(this.getSessionID(),
+						"The message was received unencrypted.");
+				return plainTextMessage.cleanText;
+			case PLAINTEXT:
+				// Simply display the message to the user. If
+				// REQUIRE_ENCRYPTION
+				// is set, warn him that the message was received
+				// unencrypted.
+				if (policy.getRequireEncryption()) {
+					getListener().showWarning(this.getSessionID(),
+							"The message was received unencrypted.");
+				}
+				return plainTextMessage.cleanText;
+			}
+		} else {
+			logger
+					.finest("Received plaintext message with the whitespace tag.");
+			switch (this.getSessionStatus()) {
+			case ENCRYPTED:
+			case FINISHED:
+				// Remove the whitespace tag and display the message to the
+				// user, but warn him that the message was received
+				// unencrypted.
+				getListener().showWarning(this.getSessionID(),
+						"The message was received unencrypted.");
+			case PLAINTEXT:
+				// Remove the whitespace tag and display the message to the
+				// user. If REQUIRE_ENCRYPTION is set, warn him that the
+				// message
+				// was received unencrypted.
+				if (policy.getRequireEncryption())
+					getListener().showWarning(this.getSessionID(),
+							"The message was received unencrypted.");
+			}
+
+			if (policy.getWhitespaceStartAKE()) {
+				logger.finest("WHITESPACE_START_AKE is set");
+
+				if (plainTextMessage.versions.contains(2)
+						&& policy.getAllowV2()) {
+					logger.finest("V2 tag found.");
+					getAuthContext().respondV2Auth();
+				} else if (plainTextMessage.versions.contains(1)
+						&& policy.getAllowV1()) {
+					throw new UnsupportedOperationException();
+				}
+			}
+		}
+
+		return plainTextMessage.cleanText;
+	}
+
+	// Retransmit last sent message. Spec document does not mention where or
+	// when that should happen, must check libotr code.
+	private String lastSentMessage;
+
+	public String transformSending(String msgText, List<TLV> tlvs)
+			throws OtrException {
+
+		switch (this.getSessionStatus()) {
+		case PLAINTEXT:
+			if (getSessionPolicy().getRequireEncryption()) {
+				this.lastSentMessage = msgText;
+				this.startSession();
+			} else
+				// TODO this does not precisly behave according to
+				// specification.
+				return msgText;
+		case ENCRYPTED:
+			this.lastSentMessage = msgText;
+			logger.finest(getSessionID().getAccountID()
+					+ " sends an encrypted message to "
+					+ getSessionID().getUserID() + " throught "
+					+ getSessionID().getProtocolName() + ".");
+
+			// Get encryption keys.
+			SessionKeys encryptionKeys = this.getEncryptionSessionKeys();
+			int senderKeyID = encryptionKeys.getLocalKeyID();
+			int receipientKeyID = encryptionKeys.getRemoteKeyID();
+
+			// Increment CTR.
+			encryptionKeys.incrementSendingCtr();
+			byte[] ctr = encryptionKeys.getSendingCtr();
+
+			ByteArrayOutputStream out = new ByteArrayOutputStream();
+			if (msgText != null && msgText.length() > 0)
+				try {
+					out.write(msgText.getBytes("UTF8"));
+				} catch (IOException e) {
+					throw new OtrException(e);
+				}
+
+			// Append tlvs
+			if (tlvs != null && tlvs.size() > 0) {
+				out.write((byte) 0x00);
+
+				OtrOutputStream eoos = new OtrOutputStream(out);
+				for (TLV tlv : tlvs) {
+					try {
+						eoos.writeShort(tlv.type);
+						eoos.writeTlvData(tlv.value);
+					} catch (IOException e) {
+						throw new OtrException(e);
+					}
+				}
+			}
+
+			OtrCryptoEngine otrCryptoEngine = new OtrCryptoEngineImpl();
+
+			byte[] data = out.toByteArray();
+			// Encrypt message.
+			logger
+					.finest("Encrypting message with keyids (localKeyID, remoteKeyID) = ("
+							+ senderKeyID + ", " + receipientKeyID + ")");
+			byte[] encryptedMsg = otrCryptoEngine.aesEncrypt(encryptionKeys
+					.getSendingAESKey(), ctr, data);
+
+			// Get most recent keys to get the next D-H public key.
+			SessionKeys mostRecentKeys = this.getMostRecentSessionKeys();
+			DHPublicKey nextDH = (DHPublicKey) mostRecentKeys.getLocalPair()
+					.getPublic();
+
+			// Calculate T.
+			MysteriousT t = new MysteriousT(2, 0, senderKeyID, receipientKeyID,
+					nextDH, ctr, encryptedMsg);
+
+			// Calculate T hash.
+			byte[] sendingMACKey = encryptionKeys.getSendingMACKey();
+
+			logger
+					.finest("Transforming T to byte[] to calculate it's HmacSHA1.");
+			byte[] serializedT;
+			try {
+				serializedT = SerializationUtils.toByteArray(t);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+
+			byte[] mac = otrCryptoEngine.sha1Hmac(serializedT, sendingMACKey,
+					SerializationConstants.TYPE_LEN_MAC);
+
+			// Get old MAC keys to be revealed.
+			byte[] oldKeys = this.collectOldMacKeys();
+			DataMessage m = new DataMessage(t, mac, oldKeys);
+
+			try {
+				return SerializationUtils.toString(m);
+			} catch (IOException e) {
+				throw new OtrException(e);
+			}
+		case FINISHED:
+			this.lastSentMessage = msgText;
+			getListener()
+					.showError(
+							sessionID,
+							"Your message to "
+									+ sessionID.getUserID()
+									+ " was not sent.  Either end your private conversation, or restart it.");
+			return null;
+		default:
+			logger.finest("Uknown message state, not processing.");
+			return msgText;
+		}
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.java.otr4j.session.ISession#startSession()
+	 */
+	public void startSession() throws OtrException {
+		if (this.getSessionStatus() == SessionStatus.ENCRYPTED)
+			return;
+
+		if (!getSessionPolicy().getAllowV2())
+			throw new UnsupportedOperationException();
+
+		this.getAuthContext().startV2Auth();
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.java.otr4j.session.ISession#endSession()
+	 */
+	public void endSession() throws OtrException {
+		SessionStatus status = this.getSessionStatus();
+		switch (status) {
+		case ENCRYPTED:
+			Vector<TLV> tlvs = new Vector<TLV>();
+			tlvs.add(new TLV(1, null));
+
+			String msg = this.transformSending(null, tlvs);
+			getListener().injectMessage(getSessionID(), msg);
+			this.setSessionStatus(SessionStatus.PLAINTEXT);
+			break;
+		case FINISHED:
+			this.setSessionStatus(SessionStatus.PLAINTEXT);
+			break;
+		case PLAINTEXT:
+			return;
+		}
+
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.java.otr4j.session.ISession#refreshSession()
+	 */
+	public void refreshSession() throws OtrException {
+		this.endSession();
+		this.startSession();
+	}
+
+	private PublicKey remotePublicKey;
+
+	private void setRemotePublicKey(PublicKey pubKey) {
+		this.remotePublicKey = pubKey;
+	}
+
+	public PublicKey getRemotePublicKey() {
+		return remotePublicKey;
+	}
+
+	private List<OtrEngineListener> listeners = new Vector<OtrEngineListener>();
+
+	public void addOtrEngineListener(OtrEngineListener l) {
+		synchronized (listeners) {
+			if (!listeners.contains(l))
+				listeners.add(l);
+		}
+
+	}
+
+	public void removeOtrEngineListener(OtrEngineListener l) {
+		synchronized (listeners) {
+			listeners.remove(l);
+		}
+	}
+
+	public OtrPolicy getSessionPolicy() {
+		return getListener().getSessionPolicy(getSessionID());
+	}
+
+	public KeyPair getLocalKeyPair() {
+		return getListener().getKeyPair(this.getSessionID());
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/SessionKeys.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,54 @@
+package net.java.otr4j.session;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.OtrException;
+
+interface SessionKeys {
+
+	public static final int Previous = 0;
+	public static final int Current = 1;
+	public static final byte HIGH_SEND_BYTE = (byte) 0x01;
+	public static final byte HIGH_RECEIVE_BYTE = (byte) 0x02;
+	public static final byte LOW_SEND_BYTE = (byte) 0x02;
+	public static final byte LOW_RECEIVE_BYTE = (byte) 0x01;
+
+	public abstract void setLocalPair(KeyPair keyPair, int localPairKeyID);
+
+	public abstract void setRemoteDHPublicKey(DHPublicKey pubKey,
+			int remoteKeyID);
+
+	public abstract void incrementSendingCtr();
+
+	public abstract byte[] getSendingCtr();
+
+	public abstract byte[] getReceivingCtr();
+
+	public abstract void setReceivingCtr(byte[] ctr);
+
+	public abstract byte[] getSendingAESKey() throws OtrException;
+
+	public abstract byte[] getReceivingAESKey() throws OtrException;
+
+	public abstract byte[] getSendingMACKey() throws OtrException;
+
+	public abstract byte[] getReceivingMACKey() throws OtrException;
+
+	public abstract void setS(BigInteger s);
+
+	public abstract void setIsUsedReceivingMACKey(Boolean isUsedReceivingMACKey);
+
+	public abstract Boolean getIsUsedReceivingMACKey();
+
+	public abstract int getLocalKeyID();
+
+	public abstract int getRemoteKeyID();
+
+	public abstract DHPublicKey getRemoteKey();
+
+	public abstract KeyPair getLocalPair();
+
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/SessionKeysImpl.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,240 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.session;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.logging.Logger;
+
+import javax.crypto.interfaces.DHPublicKey;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngine;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.io.SerializationUtils;
+
+/**
+ * 
+ * @author George Politis
+ */
+class SessionKeysImpl implements SessionKeys {
+
+	private static Logger logger = Logger.getLogger(SessionKeysImpl.class
+			.getName());
+	private String keyDescription;
+
+	public SessionKeysImpl(int localKeyIndex, int remoteKeyIndex) {
+		if (localKeyIndex == 0)
+			keyDescription = "(Previous local, ";
+		else
+			keyDescription = "(Most recent local, ";
+
+		if (remoteKeyIndex == 0)
+			keyDescription += "Previous remote)";
+		else
+			keyDescription += "Most recent remote)";
+
+	}
+
+	public void setLocalPair(KeyPair keyPair, int localPairKeyID) {
+		this.localPair = keyPair;
+		this.setLocalKeyID(localPairKeyID);
+		logger.finest(keyDescription + " current local key ID: "
+				+ this.getLocalKeyID());
+		this.reset();
+	}
+
+	public void setRemoteDHPublicKey(DHPublicKey pubKey, int remoteKeyID) {
+		this.setRemoteKey(pubKey);
+		this.setRemoteKeyID(remoteKeyID);
+		logger.finest(keyDescription + " current remote key ID: "
+				+ this.getRemoteKeyID());
+		this.reset();
+	}
+
+	private byte[] sendingCtr = new byte[16];
+	private byte[] receivingCtr = new byte[16];
+
+	public void incrementSendingCtr() {
+		logger.finest("Incrementing counter for (localkeyID, remoteKeyID) = ("
+				+ getLocalKeyID() + "," + getRemoteKeyID() + ")");
+		// logger.debug("Counter prior increament: " +
+		// Utils.dump(sendingCtr,
+		// true, 16));
+		for (int i = 7; i >= 0; i--)
+			if (++sendingCtr[i] != 0)
+				break;
+		// logger.debug("Counter after increament: " +
+		// Utils.dump(sendingCtr,
+		// true, 16));
+	}
+
+	public byte[] getSendingCtr() {
+		return sendingCtr;
+	}
+
+	public byte[] getReceivingCtr() {
+		return receivingCtr;
+	}
+
+	public void setReceivingCtr(byte[] ctr) {
+		for (int i = 0; i < ctr.length; i++)
+			receivingCtr[i] = ctr[i];
+	}
+
+	private void reset() {
+		logger.finest("Resetting " + keyDescription + " session keys.");
+		Arrays.fill(this.sendingCtr, (byte) 0x00);
+		Arrays.fill(this.receivingCtr, (byte) 0x00);
+		this.sendingAESKey = null;
+		this.receivingAESKey = null;
+		this.sendingMACKey = null;
+		this.receivingMACKey = null;
+		this.setIsUsedReceivingMACKey(false);
+		this.s = null;
+		if (getLocalPair() != null && getRemoteKey() != null) {
+			this.isHigh = ((DHPublicKey) getLocalPair().getPublic()).getY()
+					.abs().compareTo(getRemoteKey().getY().abs()) == 1;
+		}
+
+	}
+
+	private byte[] h1(byte b) throws OtrException {
+
+		try {
+			byte[] secbytes = SerializationUtils.writeMpi(getS());
+
+			int len = secbytes.length + 1;
+			ByteBuffer buff = ByteBuffer.allocate(len);
+			buff.put(b);
+			buff.put(secbytes);
+			byte[] result = new OtrCryptoEngineImpl().sha1Hash(buff.array());
+			return result;
+		} catch (Exception e) {
+			throw new OtrException(e);
+		}
+	}
+
+	public byte[] getSendingAESKey() throws OtrException {
+		if (sendingAESKey != null)
+			return sendingAESKey;
+
+		byte sendbyte = LOW_SEND_BYTE;
+		if (this.isHigh)
+			sendbyte = HIGH_SEND_BYTE;
+
+		byte[] h1 = h1(sendbyte);
+
+		byte[] key = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH];
+		ByteBuffer buff = ByteBuffer.wrap(h1);
+		buff.get(key);
+		logger.finest("Calculated sending AES key.");
+		this.sendingAESKey = key;
+		return sendingAESKey;
+	}
+
+	public byte[] getReceivingAESKey() throws OtrException {
+		if (receivingAESKey != null)
+			return receivingAESKey;
+
+		byte receivebyte = LOW_RECEIVE_BYTE;
+		if (this.isHigh)
+			receivebyte = HIGH_RECEIVE_BYTE;
+
+		byte[] h1 = h1(receivebyte);
+
+		byte[] key = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH];
+		ByteBuffer buff = ByteBuffer.wrap(h1);
+		buff.get(key);
+		logger.finest("Calculated receiving AES key.");
+		this.receivingAESKey = key;
+
+		return receivingAESKey;
+	}
+
+	public byte[] getSendingMACKey() throws OtrException {
+		if (sendingMACKey != null)
+			return sendingMACKey;
+
+		sendingMACKey = new OtrCryptoEngineImpl().sha1Hash(getSendingAESKey());
+		logger.finest("Calculated sending MAC key.");
+		return sendingMACKey;
+	}
+
+	public byte[] getReceivingMACKey() throws OtrException {
+		if (receivingMACKey == null) {
+			receivingMACKey = new OtrCryptoEngineImpl()
+					.sha1Hash(getReceivingAESKey());
+			logger.finest("Calculated receiving AES key.");
+		}
+		return receivingMACKey;
+	}
+
+	private BigInteger getS() throws OtrException {
+		if (s == null) {
+			s = new OtrCryptoEngineImpl().generateSecret(getLocalPair()
+					.getPrivate(), getRemoteKey());
+			logger.finest("Calculating shared secret S.");
+		}
+		return s;
+	}
+
+	public void setS(BigInteger s) {
+		this.s = s;
+	}
+
+	public void setIsUsedReceivingMACKey(Boolean isUsedReceivingMACKey) {
+		this.isUsedReceivingMACKey = isUsedReceivingMACKey;
+	}
+
+	public Boolean getIsUsedReceivingMACKey() {
+		return isUsedReceivingMACKey;
+	}
+
+	private void setLocalKeyID(int localKeyID) {
+		this.localKeyID = localKeyID;
+	}
+
+	public int getLocalKeyID() {
+		return localKeyID;
+	}
+
+	private void setRemoteKeyID(int remoteKeyID) {
+		this.remoteKeyID = remoteKeyID;
+	}
+
+	public int getRemoteKeyID() {
+		return remoteKeyID;
+	}
+
+	private void setRemoteKey(DHPublicKey remoteKey) {
+		this.remoteKey = remoteKey;
+	}
+
+	public DHPublicKey getRemoteKey() {
+		return remoteKey;
+	}
+
+	public KeyPair getLocalPair() {
+		return localPair;
+	}
+
+	private int localKeyID;
+	private int remoteKeyID;
+	private DHPublicKey remoteKey;
+	private KeyPair localPair;
+
+	private byte[] sendingAESKey;
+	private byte[] receivingAESKey;
+	private byte[] sendingMACKey;
+	private byte[] receivingMACKey;
+	private Boolean isUsedReceivingMACKey;
+	private BigInteger s;
+	private Boolean isHigh;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/net/java/otr4j/session/SessionStatus.java	Sun Dec 05 18:45:54 2010 +0100
@@ -0,0 +1,17 @@
+/*
+ * otr4j, the open source java otr library.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.otr4j.session;
+
+/**
+ * 
+ * @author George Politis
+ */
+public enum SessionStatus {
+	PLAINTEXT,
+	ENCRYPTED,
+	FINISHED
+}
\ No newline at end of file