package edu.washington.cs.oneswarm.f2f.invitations; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.util.logging.Logger; import org.apache.xerces.impl.dv.util.Base64; import org.gudy.azureus2.core3.logging.LogEvent; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.HashWrapper; import com.aelitis.azureus.core.networkmanager.ConnectionEndpoint; import com.aelitis.azureus.core.networkmanager.IncomingMessageQueue.MessageQueueListener; import com.aelitis.azureus.core.networkmanager.NetworkConnection; import com.aelitis.azureus.core.networkmanager.NetworkConnection.ConnectionListener; import com.aelitis.azureus.core.networkmanager.NetworkManager; import com.aelitis.azureus.core.networkmanager.impl.osssl.OneSwarmSslTransportHelperFilterStream; import com.aelitis.azureus.core.networkmanager.impl.tcp.ProtocolEndpointTCP; import com.aelitis.azureus.core.peermanager.messaging.Message; import edu.washington.cs.oneswarm.f2f.FriendInvitation; import edu.washington.cs.oneswarm.f2f.FriendInvitation.Status; import edu.washington.cs.oneswarm.f2f.Log; import edu.washington.cs.oneswarm.f2f.invitations.InvitationManager.AuthCallback; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthHandshake; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthMessage; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthMessageDecoder; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthMessageEncoder; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthRequest; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthRequest.AuthType; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthResponse; import edu.washington.cs.oneswarm.f2f.messaging.invitation.OSF2FAuthStatus; public class InvitationConnection { /** * Protocol: A->B invite + (optional pin) * * B->DHT lookup(sha1(invite[0-19])) * * DHT->B xor(sha1(invite[10-19]),ip:port) * * B->A connect ip:port * * B: verify bytes 0-9 of sha1(remote public key) with bytes[0-9] or invite * * B->A handshake * * A->B handshake * * A->B request invite * * A: look up invite code, if valid, send STATUS_INVITE_KEY_OK * * CASE: if no additional security: * * A: add B as friend * * A->B STATUS_INVITE_KEY_OK * * B: add A as friend * * * CASE: pin security, goal: Pin is sent in separate medium. A needs to * prove to B that it knows the PIN. B needs to prove to A that it knows the * PIN. Because the limited length of the pin they chain sha1 the pin to * make sure that brute force takes longer time. * * A->B STATUS_INVITE_KEY_OK * * B->A PIN_REQUEST + nounce * * A->B hash=nounce+pin; 100x: hash=sha1(hash+nounce); send hash * * A->B PIN_REQUEST + NOUNCE2 * * B now has 15s to compute the hash and send it back, otherwise the invite * is marked as expired, the 100x sha1 computation takes 1s (on my laptop) * * B: verify hash * * B->A hash=nounce2+pin; 100x: hash=sha1(hash+nounce2); send hash * * A: verify hash, add B as friend * * A->B STATUS_INVITE_KEY_OK * * B: add A as friend * * * SECURITY DISCUSSION * * NO PIN: * * Case: E intercepts invite code: * * E can use invite code to become friends with A * * Case: E replaces invite code: * * E can modify the code so B connects to and befriends E instead of A * * Case: E spoofes invite to B: * * B (if accepting the invite) will become friends with E * * WITH PIN: * * Case: E intercepts invite code: * * E will connect to A, A will prove that it knows the pin. E will not have * enought time to brute force the pin before invite is expired * * Case: E replaces invite code: * * B will connect to E, E will not be able to prove that it knows the PIN * * Case: E spoofes invite to B: * * If B only accepts invites with PIN; E will have to know a separate way to * send the PIN to B while still pretending to be A * * * INVITE CODE FORMAT: bytes[0-9] is the sha1 of A's public key, this is * used to verify that the remote host is the expected one * * bytes[0-19] are used to calculate the position in the DHT where the * ip:port (dht key is sha1 of bytes[0-19]). The value in the dht is the * ip:port xord with the sha1 of bytes[10-19] * * bytes[20-28] are left untouched to make sure that at least 9 bytes of the * invite can not be brute forced offline byte 29 is used for flags * */ private static Logger logger = Logger.getLogger(InvitationConnection.class.getName()); private Boolean redeeming = null; private final NetworkConnection connection; private final long connectionTime; private final AuthCallback callback; // the invitation corresponding to this private FriendInvitation invitation; private final boolean remoteSideAuthenticated = false; private enum ConnectionType { UNKNOWN, INVITING_INCOMING, INVITING_OUTGOING, REDEEMING_INCOMING, REDEEMING_OUTGOING; } private enum ProtocolStateInviting { NONE, HANDSHAKING, HANDSHAKE_COMPLETED, INVITE_CODE_REQUEST_SENT, INVITE_CODE_RECEIVED, AUTHENTICATED; } private enum ProtocolStateRedeeming { NONE, HANDSHAKING, HANDSHAKE_COMPLETED, INVITE_CODE_REQUEST_RECEIVED, INVITE_CODE_SENT, AUTHENTICATED; } private ProtocolStateInviting protocolStateInviting = ProtocolStateInviting.NONE; private ProtocolStateRedeeming protocolStateRedeeming = ProtocolStateRedeeming.NONE; private ConnectionType connectionType = ConnectionType.UNKNOWN; /** * outgoing * * @param _manager * @param remoteFriendAddr * @param invitation * @param _remoteFriend */ public InvitationConnection(ConnectionEndpoint remoteFriendAddr, final FriendInvitation invitation, AuthCallback callback) { if (invitation.isCreatedLocally()) { connectionType = ConnectionType.INVITING_OUTGOING; redeeming = false; } else { connectionType = ConnectionType.REDEEMING_OUTGOING; redeeming = true; } remoteFriendAddr .addProtocol(new ProtocolEndpointTCP(remoteFriendAddr.getNotionalAddress())); this.callback = callback; this.invitation = invitation; final byte[][] sharedSecret = new byte[2][0]; sharedSecret[0] = OneSwarmSslTransportHelperFilterStream.SHARED_SECRET_FOR_SSL_STRING .getBytes(); sharedSecret[1] = OneSwarmSslTransportHelperFilterStream.ANY_KEY_ACCEPTED_BYTES; this.connectionTime = System.currentTimeMillis(); logger.fine("making outgoing connection to:\n" + remoteFriendAddr); this.connection = NetworkManager.getSingleton().createConnection(remoteFriendAddr, new OSF2FAuthMessageEncoder(), new OSF2FAuthMessageDecoder(), true, false, sharedSecret); // this.hash = getHashOf(remoteFriend.getPublicKey(), // this.getRemoteIp(), this.getRemotePort()); this.connection.connect(null, false, new ConnectionListener() { @Override public void connectFailure(Throwable failure_msg) { logger.fine(connection + " : connect error: " + failure_msg.getMessage()); close(); } @Override public void connectStarted() { } @Override public void connectSuccess(ByteBuffer remaining_initial_data) { remoteKey = sharedSecret[1]; if (redeeming) { /* * check that the remote public key is correct if this is a * redeemed invitation */ if (!invitation.pubKeyMatch(remoteKey)) { // strange, we connected to the wrong place, close sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PUB_KEY); close("connected to the wrong key: remote public key=" + Base64.encode(remoteKey)); return; } protocolStateRedeeming = ProtocolStateRedeeming.HANDSHAKING; } else { /* * if this is an inviting outgoing connection we don't know * the expected remote key, we won't be able to identify it * until later */ protocolStateInviting = ProtocolStateInviting.HANDSHAKING; } connection.getIncomingMessageQueue().registerQueueListener( new IncomingQueueListener()); NetworkManager.getSingleton().startTransferProcessing(connection); sendHandshake(); enableFastMessageProcessing(true); logger.fine("made connection to: " + Base64.encode(remoteKey)); } @Override public void exceptionThrown(Throwable error) { connectionException(error); } @Override public String getDescription() { return "connection listener: OSF2F session, outgoing"; } }); } private void sendError(int code) { sendMessage(new OSF2FAuthStatus(OSF2FAuthMessage.CURRENT_VERSION, code)); } public int getRemotePort() { return connection.getEndpoint().getNotionalAddress().getPort(); } private void connectionException(Throwable error) { Log.log(LogEvent.LT_WARNING, "got exception in " + "OS Auth session (" + connection + "/" + getRemoteIp() + ") disconnecting: " + error.getMessage() + " (from: " + error.toString() + ")"); String friendLogMessage = error.getMessage(); boolean expectedError = false; if (friendLogMessage.startsWith("transport closed")) { expectedError = true; } else if (friendLogMessage.startsWith("Connection reset by peer")) { expectedError = true; } else if (friendLogMessage .startsWith("An existing connection was forcibly closed by the remote host")) { expectedError = true; } if (!expectedError) { StringWriter st = new StringWriter(); error.printStackTrace(new PrintWriter(st)); String stackTrace = st.toString(); friendLogMessage += "\n" + stackTrace; } logger.fine("got exception: " + friendLogMessage); close(); } private byte[] remoteKey; /** * creates a new incoming connection * * @param _connection * @param _remoteFriend */ public InvitationConnection(byte[] remoteKey, NetworkConnection _connection, AuthCallback callback) { this.remoteKey = remoteKey; this.callback = callback; this.connection = _connection; this.connectionTime = System.currentTimeMillis(); connection.getIncomingMessageQueue().registerQueueListener(new IncomingQueueListener()); connection.connect(true, new ConnectionListener() { @Override public void connectFailure(Throwable failure_msg) { logger.fine(connection + " : connect error: " + failure_msg.getMessage()); close(); } @Override public void connectStarted() { // nop } @Override public void connectSuccess(ByteBuffer remaining_initial_data) { logger.fine("incoming auth connection from: " + getRemoteIp().getHostAddress()); logger.fine("remote key:" + Base64.encode(InvitationConnection.this.remoteKey)); /* * check if we recognize the remote key, in that case this is an * incoming redeeming connection */ FriendInvitation i = InvitationConnection.this.callback .getInvitationFromPublicKey(InvitationConnection.this.remoteKey); if (i != null) { redeeming = true; connectionType = ConnectionType.REDEEMING_INCOMING; invitation = i; protocolStateRedeeming = ProtocolStateRedeeming.HANDSHAKING; } else { redeeming = false; connectionType = ConnectionType.INVITING_INCOMING; /* * if this is an inviting incoming connection we don't know * the expected remote key, we won't be able to identify it * until later */ protocolStateInviting = ProtocolStateInviting.HANDSHAKING; } enableFastMessageProcessing(true); NetworkManager.getSingleton().startTransferProcessing(connection); sendHandshake(); } @Override public void exceptionThrown(Throwable error) { // ok, something strange happened, // notify connection and manager logger.fine("got error: " + error.getMessage()); close("got error: " + error.getMessage()); } @Override public String getDescription() { return "connection listener: OSF2F session, incoming"; } }); } private void sendMessage(OSF2FAuthMessage message) { logger.fine("sending message: " + message.getDescription()); connection.getOutgoingMessageQueue().addMessage(message, false); } private void sendHandshake() { sendMessage(new OSF2FAuthHandshake((byte) 1, new byte[8])); } public void enableFastMessageProcessing(boolean enable) { logger.finer(this + ": setting fast message processing=" + enable); if (enable) { NetworkManager.getSingleton().upgradeTransferProcessing(connection, null); } else { // always enable this // NetworkManager.getSingleton().upgradeTransferProcessing(connection // ); } } protected void close() { logger.fine("closing connection"); connection.close(); callback.closed(this); } public InetAddress getRemoteIp() { return connection.getEndpoint().getNotionalAddress().getAddress(); } private void handleIncomingHandshake(OSF2FAuthHandshake authHandshake) { logger.fine("handshake received"); if (invitation != null) { invitation.setStatus(Status.STATUS_CONNECTED); } if (redeeming) { /* * well, the other party is in charge */ updateRedeemingStatus(ProtocolStateRedeeming.HANDSHAKE_COMPLETED); } else { updateInvitingStatus(ProtocolStateInviting.HANDSHAKE_COMPLETED); /* * request the key */ this.sendMessage(new OSF2FAuthRequest(OSF2FAuthMessage.CURRENT_VERSION, OSF2FAuthRequest.AuthType.KEY, null)); updateInvitingStatus(ProtocolStateInviting.INVITE_CODE_REQUEST_SENT); } } public void handleAuthRequest(OSF2FAuthRequest message) { AuthType type = message.getAuthType(); switch (type) { case KEY: /* * this is a key request, can only happen if we did an outgoing * connection */ if (!redeeming) { sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("got key request for inviting connection"); return; } if (protocolStateRedeeming != ProtocolStateRedeeming.HANDSHAKE_COMPLETED) { sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("hot key request before handshake completed, closing connection"); return; } // send over the key updateRedeemingStatus(ProtocolStateRedeeming.INVITE_CODE_REQUEST_RECEIVED); sendMessage(new OSF2FAuthResponse(OSF2FAuthMessage.CURRENT_VERSION, AuthType.KEY, invitation.getKey())); updateRedeemingStatus(ProtocolStateRedeeming.INVITE_CODE_SENT); break; default: sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("unknown auth request type: " + type); break; } } private void close(String string) { logger.fine("closing: " + string); close(); } private void handleAuthStatus(OSF2FAuthStatus message) { if (message.getStatus() == OSF2FAuthStatus.STATUS_INVITE_KEY_OK) { if (redeeming) { /* * outgoing */ if (protocolStateRedeeming != ProtocolStateRedeeming.INVITE_CODE_SENT) { sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("Got authenticated but never sent invite code, closing"); return; } int securityLevel = invitation.getSecurityLevel(); switch (securityLevel) { case FriendInvitation.SECURITY_LEVEL_LOW: // all fine, just add remote side authenticated(); break; case FriendInvitation.SECURITY_LEVEL_PIN: // we need to authenticate the remote side as well if (remoteSideAuthenticated) { authenticated(); } else { Debug.out("got status without remote side authenticated"); } break; default: break; } } else { /* * incoming: */ sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("got auth=authenticated when not expecting it, closing"); return; } } else { sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("got non OK authstatus message: " + message.getDescription()); } } private void authenticated() { invitation.setStatus(Status.STATUS_AUTHENTICATED); invitation.setRemotePublicKey(remoteKey); invitation.setLastConnectDate(System.currentTimeMillis()); invitation.setLastConnectIp(getRemoteIp().getHostAddress()); invitation.setLastConnectPort(getRemotePort()); try { callback.authenticated(invitation); } catch (InvalidKeyException e) { invitation.setStatus(Status.STATUS_INVALID); e.printStackTrace(); } } private void updateInvitingStatus(ProtocolStateInviting newStatus) { logger.finer("updating status: type=" + connectionType.name() + " old_status=" + protocolStateInviting.name() + " new_status=" + newStatus.name()); protocolStateInviting = newStatus; } private void updateRedeemingStatus(ProtocolStateRedeeming newStatus) { logger.finer("updating status: type=" + connectionType.name() + " old_status=" + protocolStateRedeeming.name() + " new_status=" + newStatus.name()); protocolStateRedeeming = newStatus; } private void handleAuthResponse(OSF2FAuthResponse message) { AuthType type = message.getAuthType(); switch (type) { case KEY: byte[] key = message.getResponse(); if (redeeming) { /* * we really shouldn't get a key unless this is an inviting * connection */ sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("got key response in redeeming connection, closing"); return; } /* * incoming then... */ // check that we are in the right state: if (protocolStateInviting != ProtocolStateInviting.INVITE_CODE_REQUEST_SENT) { sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("got invite key but never requested it..."); return; } updateInvitingStatus(ProtocolStateInviting.INVITE_CODE_RECEIVED); this.invitation = callback.getInvitationFromInviteKey(new HashWrapper(key)); /* * check that we acutally sent this... */ if (invitation == null || !invitation.keyEquals(key)) { String ip = getRemoteIp().getHostAddress(); callback.banIp(ip); sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_INV_KEY); logger.fine("incoming auth connection denied: " + ip); close("invalid auth response"); return; } int securityLevel = invitation.getSecurityLevel(); switch (securityLevel) { case FriendInvitation.SECURITY_LEVEL_LOW: authenticated(); sendMessage(new OSF2FAuthStatus(OSF2FAuthMessage.CURRENT_VERSION, OSF2FAuthStatus.STATUS_INVITE_KEY_OK)); updateInvitingStatus(ProtocolStateInviting.AUTHENTICATED); break; case FriendInvitation.SECURITY_LEVEL_PIN: sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); close("Security level pin not implemented yet"); break; default: sendError(OSF2FAuthStatus.STATUS_INVITE_ERR_PROTOCOL); Debug.out("unknown security type"); break; } break; default: break; } } private long lastMessageRecvTime = System.currentTimeMillis(); private class IncomingQueueListener implements MessageQueueListener { private long packetNum = 0; @Override public void dataBytesReceived(int byte_count) { lastMessageRecvTime = System.currentTimeMillis(); } @Override public boolean messageReceived(Message message) { packetNum++; lastMessageRecvTime = System.currentTimeMillis(); logger.finer(" got message: " + message.getDescription() + "\t::" + InvitationConnection.this); if (message instanceof OSF2FAuthResponse) { handleAuthResponse((OSF2FAuthResponse) message); } else if (message instanceof OSF2FAuthRequest) { handleAuthRequest((OSF2FAuthRequest) message); } else if (message instanceof OSF2FAuthStatus) { handleAuthStatus((OSF2FAuthStatus) message); } else if (message instanceof OSF2FAuthHandshake) { handleIncomingHandshake((OSF2FAuthHandshake) message); } else { Debug.out("unknown message: " + message.getDescription()); } return (true); } @Override public void protocolBytesReceived(int byte_count) { // TODO Auto-generated method stub } } }