/** * Copyright (C) 2011 Whisper Systems * Copyright (C) 2013 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.thoughtcrime.SMP; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.util.Base64; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import android.widget.ToggleButton; import org.thoughtcrime.SMP.crypto.IdentityKeyParcelable; import org.thoughtcrime.SMP.crypto.IdentityKeyUtil; import org.thoughtcrime.SMP.crypto.MasterSecret; import org.thoughtcrime.SMP.crypto.SMP.SMP; import org.thoughtcrime.SMP.crypto.SMP.SMPContentObserver; import org.thoughtcrime.SMP.crypto.SMP.SMPException; import org.thoughtcrime.SMP.crypto.SMP.SMPState; import org.thoughtcrime.SMP.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.SMP.database.DatabaseFactory; import org.thoughtcrime.SMP.database.NoSuchMessageException; import org.thoughtcrime.SMP.database.model.SMPMessageRecord; import org.thoughtcrime.SMP.recipients.Recipient; import org.thoughtcrime.SMP.recipients.RecipientFactory; import org.thoughtcrime.SMP.recipients.Recipients; import org.thoughtcrime.SMP.sms.MessageSender; import org.thoughtcrime.SMP.sms.OutgoingEncryptedSMPMessage; import org.thoughtcrime.SMP.sms.OutgoingEncryptedSMPSyncMessage; import org.thoughtcrime.SMP.sms.OutgoingSMPMessage; import org.thoughtcrime.SMP.util.Util; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.api.push.TextSecureAddress; /** * Activity for verifying identity keys. * * @author Moxie Marlinspike modifed by Ludwig */ public class VerifyIdentityActivity extends KeyScanningActivity implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { private static final String TAG = VerifyIdentityActivity.class.getSimpleName(); private Recipient recipient; private Recipients recipients; private MasterSecret masterSecret; private long threadId; private IdentityKey identityKey; private Boolean initiatorFlag = true; // ContentObserver Handler handler = new Handler(Looper.getMainLooper()); String CONVERSATION_URI = "content://textsecure/thread/"; SMPContentObserver smpContentObserver; private TextView localIdentityFingerprint; private TextView remoteIdentityFingerprint; private LinearLayout advancedIdentity; // initialise colours private final int ORANGE = Color.rgb(255, 165, 0); private final int RED = Color.rgb(255, 0, 0); private final int BLACK = Color.rgb(0, 0, 0); private final int GREEEN = Color.rgb(0, 255, 0); private final int BLUE = Color.rgb(0, 0, 255); private final int BROWN = Color.rgb(153, 76, 0); private final int PINK = Color.rgb(255, 0, 255); private final int PURPLE = Color.rgb(153, 51, 255); private final int YELLOW = Color.rgb(255, 255, 0); int[] colours = {ORANGE, RED, BLACK, GREEEN, BLUE, BROWN, PINK, PURPLE, YELLOW}; int initiatorButtonColour = 0; int responderButtonColour = 0; @Override protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { this.masterSecret = masterSecret; initiatorFlag = getIntent().getExtras().getBoolean("initiator"); Log.i(TAG, "initiator: " + initiatorFlag); getSupportActionBar().setDisplayHomeAsUpEnabled(true); if (initiatorFlag == true) { setContentView(R.layout.verify_identity_activity_initiator); Log.i(TAG, "setContentView: initiator = " + initiatorFlag.toString()); } else { setContentView(R.layout.verify_identity_activity_listener); Log.i(TAG, "setContentView: initiator = " + initiatorFlag.toString()); } initializeResources(); initializeFingerprints(); recipients = getRecipients(); threadId = getThreadId(); // register ContentObserver smpContentObserver = new SMPContentObserver(handler, getApplicationContext(), masterSecret); getContentResolver().registerContentObserver(Uri.parse (CONVERSATION_URI + threadId + "/smp"), true, smpContentObserver); // Button to toggle advanced settings (previous default) ToggleButton showAdvancedIdentity = (ToggleButton) findViewById(R.id.toggleAdvancedIdentity); showAdvancedIdentity.setOnCheckedChangeListener(this); advancedIdentity = (LinearLayout) findViewById(R.id.advancedIdentity); // set all buttons Button button0 = (Button) findViewById(R.id.Button0); Button button1 = (Button) findViewById(R.id.Button1); Button button2 = (Button) findViewById(R.id.Button2); Button button3 = (Button) findViewById(R.id.Button3); Button button4 = (Button) findViewById(R.id.Button4); Button button5 = (Button) findViewById(R.id.Button5); Button button6 = (Button) findViewById(R.id.Button6); Button button7 = (Button) findViewById(R.id.Button7); Button button8 = (Button) findViewById(R.id.Button8); if (initiatorFlag == true) { button3.setVisibility(View.INVISIBLE); initiatorButtonColour = getRandomColour(); button4.setBackgroundColor(initiatorButtonColour); button4.setEnabled(false); button5.setVisibility(View.INVISIBLE); final Button smpStartButton = (Button) findViewById(R.id.smp_start_button); smpStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { // start SMP protocol Log.i(TAG, "start smpSession as initiator"); smpSession(initiatorButtonColour, initiatorFlag); } catch (SMPException e) {} } }); } else { button0.setBackgroundColor(ORANGE); button1.setBackgroundColor(GREEEN); button2.setBackgroundColor(BLUE); button3.setBackgroundColor(BROWN); button4.setBackgroundColor(RED); button5.setBackgroundColor(YELLOW); button6.setBackgroundColor(PURPLE); button7.setBackgroundColor(BLACK); button8.setBackgroundColor(PINK); } } @Override public void onPostCreate(Bundle savedInstanceState){ super.onPostCreate(savedInstanceState); if(initiatorFlag == true) { SharedPreferences prefs = getApplicationContext() .getSharedPreferences("org.thoughtcrime.SMP.SMP", Context.MODE_PRIVATE); boolean authenticated = prefs.getBoolean("authenticated", false); Log.d(TAG, "onPostCreate() authenticated: " + prefs.getAll().toString()); setEntityVerification(authenticated); } } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { Boolean enabled = !isChecked; if (enabled) { advancedIdentity.setVisibility(View.GONE); findViewById(R.id.basicIdentity).setVisibility(View.VISIBLE); findViewById(R.id.identityScrollView).invalidate(); } else { advancedIdentity.setVisibility(View.VISIBLE); findViewById(R.id.basicIdentity).setVisibility(View.GONE); findViewById(R.id.identityScrollView).invalidate(); } } public void setVerificationProgressVisibility (boolean visible){ if (visible == true) { ProgressBar verificationProgress = (ProgressBar) findViewById(R.id.verification_progress); verificationProgress.setVisibility(View.VISIBLE); TextView verificationProgressText = (TextView) findViewById(R.id.verification_progress_text); verificationProgressText.setText("Waiting for: " + recipient.getName()); verificationProgressText.setVisibility(View.VISIBLE); } else { ProgressBar verificationProgress = (ProgressBar) findViewById(R.id.verification_progress); verificationProgress.setVisibility(View.INVISIBLE); TextView verificationProgressText = (TextView) findViewById(R.id.verification_progress_text); verificationProgressText.setVisibility(View.INVISIBLE); } } public void setEntityVerification(boolean verified){ TextView verfiedEntity = (TextView) findViewById(R.id.entity_verified); if(verified) { verfiedEntity.setText("Entity " + recipient.getName() + " has been verified."); } else { verfiedEntity.setText("Entity " + recipient.getName() + " has NOT been verified."); } } public void setResponderResultScreen(){ setContentView(R.layout.verify_identity_activity_initiator); findViewById(R.id.Button3).setVisibility(View.INVISIBLE); Button button4 = (Button) findViewById(R.id.Button4); button4.setBackgroundColor(getRandomColour()); button4.setEnabled(false); findViewById(R.id.Button5).setVisibility(View.INVISIBLE); } public void setEntityVerifiedFlag(boolean verifiedBefore){ SharedPreferences prefs = getApplicationContext() .getSharedPreferences("org.thoughtcrime.SMP.SMP", Context.MODE_PRIVATE); prefs.edit().putBoolean("authenticated", verifiedBefore).commit(); } // set listeners to all buttons if not initiator @Override public void onClick(View v) { Button selectedButton = (Button) findViewById(v.getId()); ColorDrawable selectedButtonColour = (ColorDrawable) selectedButton.getBackground(); responderButtonColour = selectedButtonColour.getColor(); try { Log.i(TAG, "start smpSession as receiver"); smpSession(responderButtonColour, initiatorFlag); } catch (SMPException e) {} } // pick random colour for initator public int getRandomColour() { return colours[Util.getSecureRandom().nextInt(colours.length)]; } // initiate the SMP session here public void smpSession(Integer secret, Boolean initiator) throws SMPException { byte[] secretByteArray; final SMPState a = new SMPState(); final SMPState b = new SMPState(); // TODO: SMP supported by opposite site?! //send SMP support request Boolean supportSMP = true; if (supportSMP == true) { Log.i(TAG, "SMP support: " + supportSMP.toString()); //logic for the current entity Alice(step1(), step3(), step5()) or Bob(step2(), step4()) if (initiatorFlag) { Log.i(TAG, "smpSession: initiatorFlag= " + initiatorFlag); setVerificationProgressVisibility(true); // set correct secret from user input + identity keys secretByteArray = (secret.toString()+IdentityKeyUtil.getIdentityKey(this) .getFingerprint()+identityKey.getFingerprint()).getBytes(); // start the protocol & send first message try { byte[] msg1 = SMP.step1(a, secretByteArray); Log.i(TAG, "sendSMPSyncMessage_step1"); sendSMPMessage(encode(msg1), true); //TODO: wait for response & enable to back channel if the other side refuses right now - but how? new Handler().postDelayed(new Runnable() { public void run() { if (!smpContentObserver.newSMPMessage()) { Log.w(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); } else if (smpContentObserver.newSMPMessage()) { Log.d(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); try { byte[] msg3 = SMP.step3(a, getNewSMPMessage()); Log.d(TAG, "sendSMPMessage_step3"); sendSMPMessage(encode(msg3), false); } catch (InvalidMessageException | SMPException e) { setEntityVerification(false); } } } }, 20000L); // wait for message from responder new Handler().postDelayed(new Runnable() { public void run() { if (!smpContentObserver.newSMPMessage()) { Log.d(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); } else if (smpContentObserver.newSMPMessage()) { Log.d(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); try { Log.i(TAG, "before SMP.step5()"); SMP.step5(a, getNewSMPMessage()); if (a.smProgState == 1) { // display toast or other indication for SMP success setVerificationProgressVisibility(false); setEntityVerification(true); // TODO: move along and do Web of Trust authentication & save contacts keys // set permanent marker for successful authentication setEntityVerifiedFlag(true); } else { Log.d(TAG, "SMP.step5() NO success!"); setEntityVerification(false); setVerificationProgressVisibility(false); } } catch (SMPException e) { } } } }, 40000L); } catch (InvalidMessageException e){} } else { Log.i(TAG, "smpSession: initiatorFlag= " + initiatorFlag); // set correct secret from user input + identity keys secretByteArray = (secret.toString()+identityKey.getFingerprint()+IdentityKeyUtil.getIdentityKey(this) .getFingerprint()).getBytes(); // initiate step2a after first message received SMP.step2a(b, getNewSMPMessage(), 123); // return message to initiator try { byte[] msg2 = SMP.step2b(b, secretByteArray); sendSMPMessage(encode(msg2), false); } catch (InvalidMessageException e) {} // wait for response from initiator new Handler().postDelayed(new Runnable() { public void run() { // Do delayed stuff! if (!smpContentObserver.newSMPMessage()) { Log.d(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); } else if (smpContentObserver.newSMPMessage()) { Log.d(TAG, "new SMP message: " + smpContentObserver.newSMPMessage()); try { Log.d(TAG, "before SMP.step4.getNewSMPMessage"); byte[] msg4 = SMP.step4(b, getNewSMPMessage()); sendSMPMessage(encode(msg4), false); Log.d(TAG, "msg4 array.length: " + msg4.length); if (b.smProgState == 1) { // display toast or other indication for SMP success Log.d(TAG, "SMP.step4() success!"); // set VerifyIdentityActivity as initiator screen and show success! setResponderResultScreen(); setVerificationProgressVisibility(false); setEntityVerification(true); // TODO: move along and do Web of Trust authentication & save contacts keys // set permanent marker for successful authentication setEntityVerifiedFlag(true); } else { Log.d(TAG, "SMP.step4() NO success!"); setResponderResultScreen(); setEntityVerification(false); setVerificationProgressVisibility(false); } } catch (InvalidMessageException | SMPException e) { } } } }, 20000L); } } else { Toast.makeText(getApplicationContext(), "SMP not supported. Please use advanced option to " + "verify identities.", Toast.LENGTH_LONG).show(); // automatically press advancedIdentity button after short time try { wait(2000); findViewById(R.id.toggleAdvancedIdentity).performClick(); } catch (InterruptedException e) {} } } private String encode(byte[] input) { String byteArrayString = new String(Base64.encode(input, 0)); return byteArrayString; } private byte[] decode(String input) { byte[] byteArray = Base64.decode(input.getBytes(), 0); return byteArray; } private byte[] getNewSMPMessage(){ // obtain correct messageId from SharedPreferences SharedPreferences prefs = getApplicationContext() .getSharedPreferences("org.thoughtcrime.SMP.SMP", Context.MODE_PRIVATE); int messageId = prefs.getInt("messageId", 0); byte[] msg = null; try { SMPMessageRecord smpMessageRecord = DatabaseFactory .getEncryptingSMPDatabase(getApplicationContext()) .getMessage(masterSecret, messageId); String msString = smpMessageRecord.getDisplayBody().toString(); msg = decode(msString); } catch (NoSuchMessageException e) {} return msg; } private void sendSMPMessage(String processMessage, boolean sync) throws InvalidMessageException { final Context context = getApplicationContext(); if (!sync) { OutgoingSMPMessage message; message = new OutgoingEncryptedSMPMessage(recipients, processMessage); new AsyncTask<OutgoingSMPMessage, Void, Long>() { @Override protected Long doInBackground(OutgoingSMPMessage... messages) { return MessageSender.send(context, masterSecret, messages[0], threadId); } }.execute(message); } else { OutgoingSMPMessage message; message = new OutgoingEncryptedSMPSyncMessage(recipients, processMessage); new AsyncTask<OutgoingSMPMessage, Void, Long>() { @Override protected Long doInBackground(OutgoingSMPMessage... messages) { return MessageSender.send(context, masterSecret, messages[0], threadId); } }.execute(message); } } private Long getThreadId (){ threadId = DatabaseFactory.getThreadDatabase(getApplicationContext()).getThreadIdFor(recipients); return threadId; } private Recipients getRecipients() { // get the single recipient for the SMP message recipients = RecipientFactory.getRecipientsFor(this, recipient, true); return recipients; } @Override public void onResume() { super.onResume(); getSupportActionBar().setTitle(R.string.AndroidManifest__verify_identity); } @Override public void onPause() { super.onPause(); getContentResolver().unregisterContentObserver(smpContentObserver); } private void initializeLocalIdentityKey() { if (!IdentityKeyUtil.hasIdentityKey(this)) { localIdentityFingerprint.setText(R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key); return; } localIdentityFingerprint.setText(IdentityKeyUtil.getIdentityKey(this).getFingerprint()); } private void initializeRemoteIdentityKey() { IdentityKeyParcelable identityKeyParcelable = getIntent().getParcelableExtra("remote_identity"); identityKey = null; if (identityKeyParcelable != null) { identityKey = identityKeyParcelable.get(); } if (identityKey == null) { identityKey = getRemoteIdentityKey(masterSecret, recipient); } if (identityKey == null) { remoteIdentityFingerprint.setText(R.string.VerifyIdentityActivity_recipient_has_no_identity_key); } else { remoteIdentityFingerprint.setText(identityKey.getFingerprint()); } } private void initializeFingerprints() { initializeLocalIdentityKey(); initializeRemoteIdentityKey(); } private void initializeResources() { this.localIdentityFingerprint = (TextView)findViewById(R.id.you_read); this.remoteIdentityFingerprint = (TextView)findViewById(R.id.friend_reads); this.recipient = RecipientFactory.getRecipientForId(this, this.getIntent().getLongExtra("recipient", -1), true); } @Override protected void initiateDisplay() { if (!IdentityKeyUtil.hasIdentityKey(this)) { Toast.makeText(this, R.string.VerifyIdentityActivity_you_don_t_have_an_identity_key_exclamation, Toast.LENGTH_LONG).show(); return; } super.initiateDisplay(); } @Override protected void initiateScan() { IdentityKey identityKey = getRemoteIdentityKey(masterSecret, recipient); if (identityKey == null) { Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation, Toast.LENGTH_LONG).show(); } else { super.initiateScan(); } } @Override protected String getScanString() { return getString(R.string.VerifyIdentityActivity_scan_their_key_to_compare); } @Override protected String getDisplayString() { return getString(R.string.VerifyIdentityActivity_get_my_key_scanned); } @Override protected IdentityKey getIdentityKeyToCompare() { return getRemoteIdentityKey(masterSecret, recipient); } @Override protected IdentityKey getIdentityKeyToDisplay() { return IdentityKeyUtil.getIdentityKey(this); } @Override protected String getNotVerifiedMessage() { return getString(R.string.VerifyIdentityActivity_warning_the_scanned_key_does_not_match_please_check_the_fingerprint_text_carefully); } @Override protected String getNotVerifiedTitle() { return getString(R.string.VerifyIdentityActivity_not_verified_exclamation); } @Override protected String getVerifiedMessage() { return getString(R.string.VerifyIdentityActivity_their_key_is_correct_it_is_also_necessary_to_verify_your_key_with_them_as_well); } @Override protected String getVerifiedTitle() { return getString(R.string.VerifyIdentityActivity_verified_exclamation); } private IdentityKey getRemoteIdentityKey(MasterSecret masterSecret, Recipient recipient) { SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); AxolotlAddress axolotlAddress = new AxolotlAddress(recipient.getNumber(), TextSecureAddress.DEFAULT_DEVICE_ID); SessionRecord record = sessionStore.loadSession(axolotlAddress); if (record == null) { return null; } return record.getSessionState().getRemoteIdentityKey(); } }