/* * 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.security.SecureRandom; import java.util.Arrays; 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.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.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 */ class AuthContextImpl implements AuthContext { public AuthContextImpl(Session session) { this.setSession(session); this.reset(); //logger.addHandler(new AndroidLogHandler()); } private Session session; private int authenticationState; SecureRandom secureRandom; 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 static Logger logger = Logger.getLogger(AuthContextImpl.class.getName()); private int getProtocolVersion() { return this.protocolVersion; } private void setProtocolVersion(int protoVersion) { this.protocolVersion = protoVersion; } 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 (secureRandom == null) secureRandom = new java.security.SecureRandom(); if (r == null) { logger.finest("Picking random key r."); r = new byte[OtrCryptoEngine.AES_KEY_BYTE_LENGTH]; secureRandom.nextBytes(r); } return r; } public 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; } public byte[] getExtraSymmetricKey() throws OtrException { return h2(EXTRA_SYMMETRIC_KEY); } 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.getLocalUserId() + " received a signature message from " + sessionID.getRemoteUserId() + " 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)) { session.showWarning("Bad 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.getLocalUserId() + " received a reveal signature message from " + sessionID.getRemoteUserId() + " 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)) { session.showWarning("Bad revealed 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.getLocalUserId() + " received a D-H key message from " + sessionID.getRemoteUserId() + " 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.getLocalUserId() + " received a D-H commit message from " + sessionID.getRemoteUserId() + " 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; } }