Signal-Android/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
Moxie Marlinspike 00d7b5c284 Better UX handling on identity key mismatches.
1) Migrate from GSON to Jackson everywhere.

2) Add support for storing identity key conflicts on message rows.

3) Add limited support for surfacing identity key conflicts in UI.
2015-02-27 12:26:09 -08:00

328 lines
16 KiB
Java

package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.groups.GroupMessageProcessor;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import ws.com.google.android.mms.MmsException;
public class PushDecryptJob extends MasterSecretJob {
public static final String TAG = PushDecryptJob.class.getSimpleName();
private final long messageId;
private final long smsMessageId;
public PushDecryptJob(Context context, long pushMessageId, String sender) {
this(context, pushMessageId, -1, sender);
}
public PushDecryptJob(Context context, long pushMessageId, long smsMessageId, String sender) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.withGroupId(sender)
.create());
this.messageId = pushMessageId;
this.smsMessageId = smsMessageId;
}
@Override
public void onAdded() {
if (KeyCachingService.getMasterSecret(context) == null) {
MessageNotifier.updateNotification(context, null, -2);
}
}
@Override
public void onRun(MasterSecret masterSecret) throws NoSuchMessageException {
PushDatabase database = DatabaseFactory.getPushDatabase(context);
TextSecureEnvelope envelope = database.get(messageId);
handleMessage(masterSecret, envelope, smsMessageId);
database.delete(messageId);
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onCanceled() {
}
private void handleMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
int deviceId = envelope.getSourceDevice();
AxolotlStore axolotlStore = new TextSecureAxolotlStore(context, masterSecret);
TextSecureCipher cipher = new TextSecureCipher(axolotlStore, recipientId, deviceId);
TextSecureMessage message = cipher.decrypt(envelope);
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
if (envelope.isPreKeyWhisperMessage()) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
}
} catch (InvalidVersionException e) {
Log.w(TAG, e);
handleInvalidVersionMessage(masterSecret, envelope, smsMessageId);
} catch (InvalidMessageException | InvalidKeyIdException | InvalidKeyException | MmsException | RecipientFormattingException e) {
Log.w(TAG, e);
handleCorruptMessage(masterSecret, envelope, smsMessageId);
} catch (NoSessionException e) {
Log.w(TAG, e);
handleNoSessionMessage(masterSecret, envelope, smsMessageId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
handleLegacyMessage(masterSecret, envelope, smsMessageId);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
handleDuplicateMessage(masterSecret, envelope, smsMessageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
handleUntrustedIdentityMessage(masterSecret, envelope, smsMessageId);
}
}
private void handleEndSessionMessage(MasterSecret masterSecret, long recipientId,
TextSecureEnvelope envelope, TextSecureMessage message,
long smsMessageId)
{
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(),
"", Optional.<TextSecureGroup>absent());
long threadId;
if (smsMessageId <= 0) {
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, incomingEndSessionMessage);
threadId = messageAndThreadId.second;
} else {
smsDatabase.markAsEndSession(smsMessageId);
threadId = smsDatabase.getThreadIdForMessage(smsMessageId);
}
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(recipientId);
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
MessageNotifier.updateNotification(context, masterSecret, threadId);
}
private void handleGroupMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId) {
GroupMessageProcessor.process(context, masterSecret, envelope, message);
if (smsMessageId > 0) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
}
}
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId)
throws MmsException
{
String localNumber = TextSecurePreferences.getLocalNumber(context);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterSecret, envelope.getSource(),
localNumber, message.getTimestamp(),
Optional.fromNullable(envelope.getRelay()),
message.getBody(),
message.getGroupInfo(),
message.getAttachments());
Pair<Long, Long> messageAndThreadId;
if (message.isSecure()) {
messageAndThreadId = database.insertSecureDecryptedMessageInbox(masterSecret, mediaMessage, -1);
} else {
messageAndThreadId = database.insertMessageInbox(masterSecret, mediaMessage, null, -1);
}
ApplicationContext.getInstance(context)
.getJobManager()
.add(new AttachmentDownloadJob(context, messageAndThreadId.first));
if (smsMessageId >= 0) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
}
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleTextMessage(MasterSecret masterSecret, TextSecureEnvelope envelope,
TextSecureMessage message, long smsMessageId)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
if (smsMessageId > 0) {
database.updateBundleMessageBody(masterSecret, smsMessageId, body);
} else {
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(), body,
message.getGroupInfo());
if (message.isSecure()) {
textMessage = new IncomingEncryptedMessage(textMessage, body);
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
}
private void handleInvalidVersionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsInvalidVersionKeyExchange(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId);
}
}
private void handleCorruptMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsDecryptFailed(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsDecryptFailed(smsMessageId);
}
}
private void handleNoSessionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsNoSession(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsNoSession(smsMessageId);
}
}
private void handleLegacyMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsLegacyVersion(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsLegacyVersion(smsMessageId);
}
}
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
// Let's start ignoring these now
// SmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
//
// if (smsMessageId <= 0) {
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
// smsDatabase.markAsDecryptDuplicate(messageAndThreadId.first);
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
// } else {
// smsDatabase.markAsDecryptDuplicate(smsMessageId);
// }
}
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
try {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
PreKeyWhisperMessage whisperMessage = new PreKeyWhisperMessage(envelope.getMessage());
IdentityKey identityKey = whisperMessage.getIdentityKey();
String encoded = Base64.encodeBytes(envelope.getMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
Optional.<TextSecureGroup>absent());
if (smsMessageId <= 0) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, bundleMessage);
database.addMismatchedIdentity(messageAndThreadId.first, recipientId, identityKey);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
database.updateMessageBody(masterSecret, smsMessageId, encoded);
database.markAsPreKeyBundle(smsMessageId);
database.addMismatchedIdentity(smsMessageId, recipientId, identityKey);
}
} catch (RecipientFormattingException | InvalidMessageException | InvalidVersionException e) {
throw new AssertionError(e);
}
}
private Pair<Long, Long> insertPlaceholder(MasterSecret masterSecret, TextSecureEnvelope envelope) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), "",
Optional.<TextSecureGroup>absent());
textMessage = new IncomingEncryptedMessage(textMessage, "");
return database.insertMessageInbox(masterSecret, textMessage);
}
}