package org.awesomeapp.messenger.crypto.otr; // Originally: package com.zadov.beem; import org.awesomeapp.messenger.ImApp; import org.awesomeapp.messenger.plugin.xmpp.XmppAddress; import org.awesomeapp.messenger.push.PushManager; import org.awesomeapp.messenger.push.WhitelistTokenTlvHandler; import org.awesomeapp.messenger.ui.legacy.SmpResponseActivity; import org.awesomeapp.messenger.model.Contact; import org.awesomeapp.messenger.model.Message; import org.awesomeapp.messenger.service.adapters.ImConnectionAdapter; import org.awesomeapp.messenger.service.ImServiceConstants; import org.awesomeapp.messenger.service.RemoteImService; import org.awesomeapp.messenger.util.Debug; import org.chatsecure.pushsecure.PushSecureClient; import java.io.UnsupportedEncodingException; import java.security.KeyPair; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import net.java.otr4j.OtrEngineImpl; import net.java.otr4j.OtrEngineListener; import net.java.otr4j.OtrException; import net.java.otr4j.OtrKeyManager; import net.java.otr4j.OtrPolicy; import net.java.otr4j.OtrPolicyImpl; import net.java.otr4j.session.OtrSm; import net.java.otr4j.session.OtrSm.OtrSmEngineHost; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionStatus; import net.java.otr4j.session.TLV; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; /* * OtrChatManager keeps track of the status of chats and their OTR stuff */ public class OtrChatManager implements OtrEngineListener, OtrSmEngineHost { //the singleton instance private static OtrChatManager mInstance; private OtrEngineHostImpl mOtrEngineHost; private OtrEngineImpl mOtrEngine; private Hashtable<String, SessionID> mSessions; private Hashtable<String, OtrSm> mOtrSms; private Context mContext; private ImApp mApp; // ChatSecure-Push private Hashtable<String, WhitelistTokenTlvHandler> mWhitelistTokenHandlers; private HashSet<String> mWhitelistTokenExchangedSessions; private final static String TAG = "OtrChatManager"; private OtrChatManager(int otrPolicy, RemoteImService imService, OtrAndroidKeyManagerImpl otrKeyManager) throws Exception { mContext = (Context)imService; mOtrEngineHost = new OtrEngineHostImpl(new OtrPolicyImpl(otrPolicy), mContext, otrKeyManager, imService); mOtrEngine = new OtrEngineImpl(mOtrEngineHost); mOtrEngine.addOtrEngineListener(this); mSessions = new Hashtable<String, SessionID>(); mOtrSms = new Hashtable<String, OtrSm>(); // Use the Application-managed PushManager which has a push account already authenticated mWhitelistTokenHandlers = new Hashtable<>(); mWhitelistTokenExchangedSessions = new HashSet<>(); mApp = ((ImApp)mContext.getApplicationContext()); } public static synchronized OtrChatManager getInstance(int otrPolicy, RemoteImService imService, OtrAndroidKeyManagerImpl otrKeyManager) throws Exception { if (mInstance == null) { mInstance = new OtrChatManager(otrPolicy, imService,otrKeyManager); } return mInstance; } public static OtrChatManager getInstance() { return mInstance; } public static void endAllSessions() { if (mInstance == null) { return; } Collection<SessionID> sessionIDs = mInstance.mSessions.values(); for (SessionID sessionId : sessionIDs) { mInstance.endSession(sessionId); } } public static void endSessionsForAccount(Contact localUserContact) { if (mInstance == null) { return; } String localUserId = localUserContact.getAddress().getBareAddress(); Enumeration<String> sKeys = mInstance.mSessions.keys(); while (sKeys.hasMoreElements()) { String sKey = sKeys.nextElement(); if (sKey.contains(localUserId)) { SessionID sessionId = mInstance.mSessions.get(sKey); if (sessionId != null) mInstance.endSession(sessionId); } } } public void addOtrEngineListener(OtrEngineListener oel) { mOtrEngine.addOtrEngineListener(oel); } public void setPolicy(int otrPolicy) { mOtrEngineHost.setSessionPolicy(new OtrPolicyImpl(otrPolicy)); } public OtrKeyManager getKeyManager() { return mOtrEngineHost.getKeyManager(); } public static String processResource(String userId) { String[] splits = userId.split("/", 2); if (splits.length > 1) return splits[1]; else return "UNKNOWN"; } private final static String SESSION_TYPE_XMPP = "XMPP"; public SessionID getSessionId(String localUserId, String remoteUserId) { //if (!remoteUserId.contains("/")) // Log.w(ImApp.LOG_TAG,"resource is not set: " + remoteUserId); // boolean stripResource = !remoteUserId.startsWith("group"); //SessionID sIdTemp = new SessionID(localUserId, XmppAddress.stripResource(remoteUserId), SESSION_TYPE_XMPP); SessionID sIdTemp = new SessionID(localUserId, remoteUserId, SESSION_TYPE_XMPP); SessionID sessionId = mSessions.get(sIdTemp.toString()); if (sessionId == null) { sIdTemp = new SessionID(localUserId, XmppAddress.stripResource(remoteUserId), SESSION_TYPE_XMPP); sessionId = mSessions.get(sIdTemp.toString()); if (sessionId != null) return sessionId; } if (sessionId == null) { // or we didn't have a session yet. sessionId = sIdTemp; mSessions.put(sessionId.toString(), sessionId); } else if ((!sessionId.getRemoteUserId().equals(remoteUserId)) && remoteUserId.contains("/")) { // Remote has changed (either different presence, or from generic JID to specific presence), // Create or replace sessionId with one that is specific to the new presence. sessionId = sIdTemp; mSessions.put(sessionId.toString(), sessionId); if (Debug.DEBUG_ENABLED) Log.d(ImApp.LOG_TAG,"getting new otr session id: " + sessionId); } return sessionId; } /** * Tell if the session represented by a local user account and a remote user * account is currently encrypted or not. * * @param localUserId * @param remoteUserId * @return state */ public SessionStatus getSessionStatus(String localUserId, String remoteUserId) { SessionID sessionId = getSessionId(localUserId, remoteUserId); if (sessionId == null) return null; return mOtrEngine.getSessionStatus(sessionId); } public SessionStatus getSessionStatus(SessionID sessionId) { return mOtrEngine.getSessionStatus(sessionId); } /** public void refreshSession(String localUserId, String remoteUserId) { try { mOtrEngine.refreshSession(getSessionId(localUserId, remoteUserId)); } catch (OtrException e) { OtrDebugLogger.log("refreshSession", e); } }*/ /** * Start a new OTR encryption session for the chat session represented by a * local user address and a remote user address. * * @param localUserId i.e. the account of the user of this phone * @param remoteUserId i.e. the account that this user is talking to */ private SessionID startSession(String localUserId, String remoteUserId) throws Exception { if (!remoteUserId.contains("/")) throw new Exception("can't start session without JabberID: " + localUserId); SessionID sessionId = getSessionId(localUserId, remoteUserId); try { mOtrEngine.startSession(sessionId); return sessionId; } catch (Exception e) { OtrDebugLogger.log("startSession", e); showError(sessionId, "Unable to start OTR session: " + e.getLocalizedMessage()); } return null; } /** * Start a new OTR encryption session for the chat session represented by a * {@link SessionID}. * * @param sessionId the {@link SessionID} of the OTR session */ public SessionID startSession(SessionID sessionId) { try { mOtrEngine.startSession(sessionId); return sessionId; } catch (Exception e) { OtrDebugLogger.log("startSession", e); showError(sessionId,"Unable to start OTR session: " + e.getLocalizedMessage()); } return null; } public void endSession(SessionID sessionId) { try { mOtrEngine.endSession(sessionId); mSessions.remove(sessionId.toString()); } catch (Exception e) { OtrDebugLogger.log("endSession", e); } } public void endSession(String localUserId, String remoteUserId) { SessionID sessionId = getSessionId(localUserId, remoteUserId); endSession(sessionId); } public void status(String localUserId, String remoteUserId) { mOtrEngine.getSessionStatus(getSessionId(localUserId, remoteUserId)).toString(); } public String decryptMessage(String localUserId, String remoteUserId, String msg, List<TLV> tlvs) throws OtrException { String plain = null; SessionID sessionId = getSessionId(localUserId, remoteUserId); // OtrDebugLogger.log("session status: " + mOtrEngine.getSessionStatus(sessionId)); if (mOtrEngine != null && sessionId != null) { mOtrEngineHost.putSessionResource(sessionId, processResource(remoteUserId)); plain = mOtrEngine.transformReceiving(sessionId, msg, tlvs); OtrSm otrSm = mOtrSms.get(sessionId.toString()); if (otrSm != null) { List<TLV> smTlvs = otrSm.getPendingTlvs(); if (smTlvs != null) { String encrypted = mOtrEngine.transformSending(sessionId, "", smTlvs); mOtrEngineHost.injectMessage(sessionId, encrypted); } } //not null, but empty so make it null! if (TextUtils.isEmpty(plain)) return null; } return plain; } public boolean transformSending(Message message) { return transformSending(message, false, null); } public boolean transformSending(Message message, boolean isResponse, byte[] data) { String localUserId = message.getFrom().getAddress(); String remoteUserId = message.getTo().getAddress(); String body = message.getBody(); SessionID sessionId = getSessionId(localUserId, remoteUserId); if (mOtrEngine != null && sessionId != null) { SessionStatus sessionStatus = mOtrEngine.getSessionStatus(sessionId); if (data != null && sessionStatus != SessionStatus.ENCRYPTED) { // Cannot send data without OTR, so start a session and drop message. // Message will be resent by caller when session is encrypted. startSession(sessionId); OtrDebugLogger.log("auto-start OTR on data send request"); return false; } OtrDebugLogger.log("session status: " + sessionStatus); try { OtrPolicy sessionPolicy = getSessionPolicy(sessionId); if (sessionStatus == SessionStatus.PLAINTEXT && sessionPolicy.getRequireEncryption()) { startSession(sessionId); return false; } if (sessionStatus != SessionStatus.PLAINTEXT || sessionPolicy.getRequireEncryption()) { body = mOtrEngine.transformSending(sessionId, body, isResponse, data); if (!message.getTo().getAddress().contains("/")) message.setTo(mOtrEngineHost.appendSessionResource(sessionId, message.getTo())); } else if (sessionStatus == SessionStatus.PLAINTEXT && sessionPolicy.getAllowV2() && sessionPolicy.getSendWhitespaceTag()) { // Work around asmack not sending whitespace tag for auto discovery body += " \t \t\t\t\t \t \t \t \t \t \t \t\t \t "; } } catch (Exception e) { OtrDebugLogger.log("error encrypting", e); return false; } } message.setBody(body); return true; } @Override public void sessionStatusChanged(final SessionID sessionID) { SessionStatus sStatus = mOtrEngine.getSessionStatus(sessionID); OtrDebugLogger.log("session status changed: " + sStatus); final Session session = mOtrEngine.getSession(sessionID); OtrSm otrSm = mOtrSms.get(sessionID.toString()); WhitelistTokenTlvHandler tokenTlvHandler = mWhitelistTokenHandlers.get(sessionID.toString()); if (sStatus == SessionStatus.ENCRYPTED) { PublicKey remoteKey = mOtrEngine.getRemotePublicKey(sessionID); mOtrEngineHost.storeRemoteKey(sessionID, remoteKey); if (otrSm == null) { // SMP handler - make sure we only add this once per session! otrSm = new OtrSm(session, mOtrEngineHost.getKeyManager(), sessionID, OtrChatManager.this); session.addTlvHandler(otrSm); mOtrSms.put(sessionID.toString(), otrSm); } if (tokenTlvHandler == null) { // ChatSecure-Push Whitelist Token Handler - One per session tokenTlvHandler = new WhitelistTokenTlvHandler( mApp.getPushManager(), sessionID, new WhitelistTokenTlvHandler.TlvSender() { @Override public void onSendRequested(@NonNull TLV tlv, @NonNull SessionID sessionId) { ArrayList<TLV> tlvList = new ArrayList<>(1); tlvList.add(tlv); String encrypted; try { encrypted = mOtrEngine.transformSending(sessionId, "", tlvList); mOtrEngineHost.injectMessage(sessionId, encrypted); // We only need to perform the Whitelist Token TLV Exchange once per-session // After we've responded to this session's TLV exchange, remove TLV handler WhitelistTokenTlvHandler tokenTlvHandler = mWhitelistTokenHandlers.remove(sessionId.toString()); Session session = mOtrEngine.getSession(sessionId); session.removeTlvHandler(tokenTlvHandler); } catch (Exception e) { Log.d(TAG, "Failed to encrypt outbound Whitelist Token TLV"); } } }); session.addTlvHandler(tokenTlvHandler); mWhitelistTokenHandlers.put(sessionID.toString(), tokenTlvHandler); // Ensure we have a ChatSecure-Push Whitelist Token available // to send to this Session's participant when the first message is sent /** * /// we don't use tokens yet since we don't receive pushes mPushManager.createReceivingWhitelistTokenForPeer( PushManager.stripJabberIdResource(sessionID.getLocalUserId()), PushManager.stripJabberIdResource(sessionID.getRemoteUserId()), new PushSecureClient.RequestCallback<PersistedPushToken>() { @Override public void onSuccess(@NonNull PersistedPushToken response) { if (Debug.DEBUG_ENABLED) Log.d(TAG, "Prepared push whitelist token for " + sessionID.getRemoteUserId()); // the token has already been persisted by pushManager } @Override public void onFailure(@NonNull Throwable t) { if (Debug.DEBUG_ENABLED) Log.e(TAG, "Failed to prepare push whitelist token for " + sessionID.getRemoteUserId(), t); } }); */ } } else if (sStatus == SessionStatus.PLAINTEXT) { if (otrSm != null) { session.removeTlvHandler(otrSm); mOtrSms.remove(sessionID.toString()); } if (tokenTlvHandler != null) { session.removeTlvHandler(tokenTlvHandler); mWhitelistTokenHandlers.remove(sessionID.toString()); } mOtrEngineHost.removeSessionResource(sessionID); } else if (sStatus == SessionStatus.FINISHED) { // Do nothing. The user must take affirmative action to // restart or end the session, so that they don't send // plaintext by mistake. } } public boolean isRemoteKeyVerified (String userId, String fingerprint) { return mOtrEngineHost.isRemoteKeyVerified(userId, fingerprint); } public String getRemoteKeyFingerprint (String userId) { return mOtrEngineHost.getRemoteKeyFingerprint(userId); } public ArrayList<String> getRemoteKeyFingerprints (String userId) { return mOtrEngineHost.getRemoteKeyFingerprints(userId); } public boolean hasRemoteKeyFingerprint (String userId) { return mOtrEngineHost.hasRemoteKeyFingerprint(userId); } public String getLocalKeyFingerprint(String localUserId) { return mOtrEngineHost.getLocalKeyFingerprint(localUserId); } @Override public void injectMessage(SessionID sessionID, String msg) { mOtrEngineHost.injectMessage(sessionID, msg); } @Override public void showWarning(SessionID sessionID, String warning) { mOtrEngineHost.showWarning(sessionID, warning); } @Override public void showError(SessionID sessionID, String error) { mOtrEngineHost.showError(sessionID, error); } @Override public OtrPolicy getSessionPolicy(SessionID sessionID) { return mOtrEngineHost.getSessionPolicy(sessionID); } @Override public KeyPair getKeyPair(SessionID sessionID) { return mOtrEngineHost.getKeyPair(sessionID); } @Override public void askForSecret(SessionID sessionID, String question) { Intent dialog = new Intent(mContext.getApplicationContext(), SmpResponseActivity.class); dialog.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); dialog.putExtra("q", question); dialog.putExtra("sid", sessionID.getRemoteUserId());//yes "sid" = remoteUserId in this case - see SMPResponseActivity ImConnectionAdapter connection = mOtrEngineHost.findConnection(sessionID); if (connection == null) { OtrDebugLogger.log("Could ask for secret - no connection for " + sessionID.toString()); return; } dialog.putExtra(ImServiceConstants.EXTRA_INTENT_PROVIDER_ID, connection.getProviderId()); mContext.getApplicationContext().startActivity(dialog); } public void respondSmp(SessionID sessionID, String secret) throws OtrException { OtrSm otrSm = mOtrSms.get(sessionID.toString()); List<TLV> tlvs; if (otrSm == null) { showError(sessionID, "Could not respond to verification because conversation is not encrypted"); return; } tlvs = otrSm.initRespondSmp(null, secret, false); String encrypted = mOtrEngine.transformSending(sessionID, "", tlvs); mOtrEngineHost.injectMessage(sessionID, encrypted); } public void initSmp(SessionID sessionID, String question, String secret) throws OtrException { OtrSm otrSm = mOtrSms.get(sessionID.toString()); List<TLV> tlvs; if (otrSm == null) { showError(sessionID, "Could not perform verification because conversation is not encrypted"); return; } tlvs = otrSm.initRespondSmp(question, secret, true); String encrypted = mOtrEngine.transformSending(sessionID, "", tlvs); mOtrEngineHost.injectMessage(sessionID, encrypted); } public void abortSmp(SessionID sessionID) throws OtrException { OtrSm otrSm = mOtrSms.get(sessionID.toString()); if (otrSm == null) return; List<TLV> tlvs = otrSm.abortSmp(); String encrypted = mOtrEngine.transformSending(sessionID, "", tlvs); mOtrEngineHost.injectMessage(sessionID, encrypted); } /** * Create a message body describing the ChatSecure-Push Whitelist Token Exchange. * See <a href="https://github.com/ChatSecure/ChatSecure-Push-Server/wiki/Chat-Client-Implementation-Notes#json-whitelist-token-exchange">JSON Whitelist Token Exchange</a> * * @param message A {@link Message} providing the 'to' & 'from' addresses, as well as * any message body text (this is currently unused by the ChatSecure-Push * spec). * @param whitelistTokens An Array of one or more ChatSecure-Push Whitelist tokens */ public boolean transformPushWhitelistTokenSending(@NonNull Message message, @NonNull String[] whitelistTokens) { String localUserId = message.getFrom().getAddress(); String remoteUserId = message.getTo().getAddress(); String body = message.getBody(); SessionID sessionId = getSessionId(localUserId, remoteUserId); try { TLV tokenTlv = mApp.getPushManager().createWhitelistTokenExchangeTlvWithToken(whitelistTokens, null); List<TLV> tlvs = new ArrayList<>(1); tlvs.add(tokenTlv); if (mOtrEngine != null && sessionId != null) { SessionStatus sessionStatus = mOtrEngine.getSessionStatus(sessionId); if (sessionStatus != SessionStatus.ENCRYPTED) { // Cannot send Whitelist token without OTR. // TODO: Is it possible to Postpone-send a TLV message? OtrDebugLogger.log("Could not send ChatSecure-Push Whitelist Token TLV. Session not encrypted."); return false; } OtrDebugLogger.log("session status: " + sessionStatus); body = mOtrEngine.transformSending(sessionId, body, tlvs); message.setTo(mOtrEngineHost.appendSessionResource(sessionId, message.getTo())); } message.setBody(body); return true; } catch (UnsupportedEncodingException e) { Log.e(ImApp.LOG_TAG, "Failed to craft ChatSecure-Push Whitelist Token TLV", e); return false; } catch (Exception e) { OtrDebugLogger.log("error encrypting", e); return false; } } /** * Begins the ChatSecure-Push Whitelist Token Exchange if not yet performed for the * given {@param sessionID}. */ public void maybeBeginPushWhitelistTokenExchange(@NonNull final SessionID sessionID) { if (mWhitelistTokenExchangedSessions.contains(sessionID.toString())) return; mWhitelistTokenExchangedSessions.add(sessionID.toString()); try { mApp.getPushManager().createWhitelistTokenExchangeTlv( PushManager.stripJabberIdResource(sessionID.getLocalUserId()), PushManager.stripJabberIdResource(sessionID.getRemoteUserId()), new PushSecureClient.RequestCallback<TLV>() { @Override public void onSuccess(@NonNull TLV response) { try { ArrayList<TLV> outboundTlvs = new ArrayList<>(); outboundTlvs.add(response); String encrypted = mOtrEngine.transformSending(sessionID, "", outboundTlvs); mOtrEngineHost.injectMessage(sessionID, encrypted); if (Debug.DEBUG_ENABLED) Log.d(TAG, "Began Push Whitelist Token TLV Exchange"); } catch (Exception e) { Log.e(TAG, "Failed to encrypt outbound Whitelist Token TLV"); mWhitelistTokenExchangedSessions.remove(sessionID.toString()); } } @Override public void onFailure(@NonNull Throwable t) { Log.e(TAG, "Failed to obtain Whitelist Token", t); mWhitelistTokenExchangedSessions.remove(sessionID.toString()); } }, null); } catch (UnsupportedEncodingException e) { Log.e(TAG, "Failed to begin Push Whitelist Token Exchange", e); mWhitelistTokenExchangedSessions.remove(sessionID.toString()); } } /** * Send a ChatSecure-Push "Knock" Push Message to the remote peer in the * given {@param sessionID}. */ public void sendKnockPushMessage(@NonNull final SessionID sessionID) { mApp.getPushManager().sendPushMessageToPeer( PushManager.stripJabberIdResource(sessionID.getLocalUserId()), PushManager.stripJabberIdResource(sessionID.getRemoteUserId()), new PushSecureClient.RequestCallback<org.chatsecure.pushsecure.response.Message>() { @Override public void onSuccess(@NonNull org.chatsecure.pushsecure.response.Message response) { if (Debug.DEBUG_ENABLED) Log.d(TAG, "Sent push message to " + sessionID.getRemoteUserId()); } @Override public void onFailure(@NonNull Throwable t) { if (Debug.DEBUG_ENABLED) Log.d(TAG, "Failed to send push message to " + sessionID.getRemoteUserId()); } }); try { mOtrEngine.clearSession(sessionID); mOtrEngineHost.removeSessionResource(sessionID); } catch (Exception e) { e.printStackTrace(); } } /** * Send a ChatSecure-Push "Knock" Push Message to the remote peer in the * given {@param sessionID}. */ public void sendKnockPushMessage(@NonNull final String localId, final String remoteId) { mApp.getPushManager().sendPushMessageToPeer( PushManager.stripJabberIdResource(localId), PushManager.stripJabberIdResource(remoteId), new PushSecureClient.RequestCallback<org.chatsecure.pushsecure.response.Message>() { @Override public void onSuccess(@NonNull org.chatsecure.pushsecure.response.Message response) { if (Debug.DEBUG_ENABLED) Log.d(TAG, "Sent push message to " + remoteId); } @Override public void onFailure(@NonNull Throwable t) { if (Debug.DEBUG_ENABLED) Log.d(TAG, "Failed to send push message to " + remoteId); } }); } /** * Send a ChatSecure-Push "Knock" Push Message to the remote peer in the * given {@param sessionID}. */ public boolean canDoKnockPushMessage(@NonNull final SessionID sessionID) { if ( mApp.getPushManager() != null) { return mApp.getPushManager().hasPersistedWhitelistToken( PushManager.stripJabberIdResource(sessionID.getRemoteUserId()), PushManager.stripJabberIdResource(sessionID.getLocalUserId())); } return false; } }