/** * Copyright (C) 2011 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 . */ package org.thoughtcrime.securesms; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.view.View; import android.widget.TextView; import com.afollestad.materialdialogs.MaterialDialog; import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.jobs.PushDecryptJob; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; import org.whispersystems.textsecure.api.messages.TextSecureGroup; import java.io.IOException; /** * Activity for displaying sent/received session keys. * * @author Moxie Marlinspike */ public class ReceiveKeyDialog extends MaterialDialog { private static final String TAG = ReceiveKeyDialog.class.getSimpleName(); private ReceiveKeyDialog(Builder builder, MessageRecord messageRecord, IdentityKey identityKey) { super(builder); initializeText(messageRecord, identityKey); } public static @NonNull ReceiveKeyDialog build(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull MessageRecord messageRecord) { try { final IncomingPreKeyBundleMessage message = getMessage(messageRecord); final IdentityKey identityKey = getIdentityKey(message); Builder builder = new Builder(context).customView(R.layout.receive_key_dialog, true) .positiveText(R.string.receive_key_dialog__complete) .negativeText(android.R.string.cancel) .callback(new ReceiveKeyDialogCallback(context, masterSecret, messageRecord, message, identityKey)); return new ReceiveKeyDialog(builder, messageRecord, identityKey); } catch (InvalidKeyException | InvalidVersionException | InvalidMessageException | LegacyMessageException e) { throw new AssertionError(e); } } private void initializeText(final MessageRecord messageRecord, final IdentityKey identityKey) { if (getCustomView() == null) { throw new AssertionError("CustomView should not be null in ReceiveKeyDialog."); } TextView descriptionText = (TextView) getCustomView().findViewById(R.id.description_text); String introText = getContext().getString(R.string.ReceiveKeyActivity_the_signature_on_this_key_exchange_is_different); SpannableString spannableString = new SpannableString(introText + " " + getContext().getString(R.string.ReceiveKeyActivity_you_may_wish_to_verify_this_contact)); spannableString.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { Intent intent = new Intent(getContext(), VerifyIdentityActivity.class); intent.putExtra("recipient", messageRecord.getIndividualRecipient().getRecipientId()); intent.putExtra("remote_identity", new IdentityKeyParcelable(identityKey)); getContext().startActivity(intent); } }, introText.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); descriptionText.setText(spannableString); descriptionText.setMovementMethod(LinkMovementMethod.getInstance()); } private static IncomingPreKeyBundleMessage getMessage(MessageRecord messageRecord) throws InvalidKeyException, InvalidVersionException, InvalidMessageException, LegacyMessageException { IncomingTextMessage message = new IncomingTextMessage(messageRecord.getIndividualRecipient().getNumber(), messageRecord.getRecipientDeviceId(), System.currentTimeMillis(), messageRecord.getBody().getBody(), Optional.absent()); return new IncomingPreKeyBundleMessage(message, message.getMessageBody()); } private static IdentityKey getIdentityKey(IncomingPreKeyBundleMessage message) throws InvalidKeyException, InvalidVersionException, InvalidMessageException, LegacyMessageException { try { return new PreKeyWhisperMessage(Base64.decode(message.getMessageBody())).getIdentityKey(); } catch (IOException e) { throw new AssertionError(e); } } private static class ReceiveKeyDialogCallback extends ButtonCallback { private Context context; private MasterSecret masterSecret; private MessageRecord messageRecord; private IncomingPreKeyBundleMessage message; private IdentityKey identityKey; public ReceiveKeyDialogCallback(Context context, MasterSecret masterSecret, MessageRecord messageRecord, IncomingPreKeyBundleMessage message, IdentityKey identityKey) { this.context = context; this.masterSecret = masterSecret; this.messageRecord = messageRecord; this.message = message; this.identityKey = identityKey; } @Override public void onPositive(MaterialDialog dialog) { new VerifyAsyncTask(context, masterSecret, messageRecord, message, identityKey).execute(); } } private static class VerifyAsyncTask extends ProgressDialogAsyncTask { private MasterSecret masterSecret; private MessageRecord messageRecord; private IncomingPreKeyBundleMessage message; private IdentityKey identityKey; public VerifyAsyncTask(Context context, MasterSecret masterSecret, MessageRecord messageRecord, IncomingPreKeyBundleMessage message, IdentityKey identityKey) { super(context, R.string.ReceiveKeyActivity_processing, R.string.ReceiveKeyActivity_processing_key_exchange); this.masterSecret = masterSecret; this.messageRecord = messageRecord; this.message = message; this.identityKey = identityKey; } @Override protected Void doInBackground(Void... params) { if (getContext() == null) return null; IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext()); PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext()); identityDatabase.saveIdentity(masterSecret, messageRecord.getIndividualRecipient().getRecipientId(), identityKey); try { byte[] body = Base64.decode(message.getMessageBody()); TextSecureEnvelope envelope = new TextSecureEnvelope(3, message.getSender(), message.getSenderDeviceId(), "", message.getSentTimestampMillis(), body); long pushId = pushDatabase.insert(envelope); ApplicationContext.getInstance(getContext()) .getJobManager() .add(new PushDecryptJob(getContext(), pushId, messageRecord.getId(), message.getSender())); } catch (IOException e) { throw new AssertionError(e); } return null; } } }