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.
328 lines
16 KiB
Java
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);
|
|
}
|
|
}
|