package org.smssecure.smssecure.jobs;
import android.content.Context;
import android.util.Log;
import org.smssecure.smssecure.crypto.AsymmetricMasterCipher;
import org.smssecure.smssecure.crypto.AsymmetricMasterSecret;
import org.smssecure.smssecure.crypto.KeyExchangeInitiator;
import org.smssecure.smssecure.crypto.MasterSecret;
import org.smssecure.smssecure.crypto.MasterSecretUtil;
import org.smssecure.smssecure.crypto.SecurityEvent;
import org.smssecure.smssecure.crypto.SmsCipher;
import org.smssecure.smssecure.crypto.storage.SilenceSignalProtocolStore;
import org.smssecure.smssecure.database.DatabaseFactory;
import org.smssecure.smssecure.database.EncryptingSmsDatabase;
import org.smssecure.smssecure.database.NoSuchMessageException;
import org.smssecure.smssecure.database.model.SmsMessageRecord;
import org.smssecure.smssecure.jobs.requirements.MasterSecretRequirement;
import org.smssecure.smssecure.notifications.MessageNotifier;
import org.smssecure.smssecure.recipients.RecipientFactory;
import org.smssecure.smssecure.recipients.Recipients;
import org.smssecure.smssecure.service.KeyCachingService;
import org.smssecure.smssecure.sms.IncomingEncryptedMessage;
import org.smssecure.smssecure.sms.IncomingEndSessionMessage;
import org.smssecure.smssecure.sms.IncomingKeyExchangeMessage;
import org.smssecure.smssecure.sms.IncomingPreKeyBundleMessage;
import org.smssecure.smssecure.sms.IncomingTextMessage;
import org.smssecure.smssecure.sms.IncomingXmppExchangeMessage;
import org.smssecure.smssecure.sms.MessageSender;
import org.smssecure.smssecure.sms.OutgoingKeyExchangeMessage;
import org.smssecure.smssecure.util.SilencePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.InvalidVersionException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.StaleKeyExchangeException;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
public class SmsDecryptJob extends MasterSecretJob {
private static final String TAG = SmsDecryptJob.class.getSimpleName();
private final long messageId;
private final boolean manualOverride;
private final Boolean isReceivedWhenLocked;
public SmsDecryptJob(Context context, long messageId, boolean manualOverride, boolean isReceivedWhenLocked) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.create());
this.messageId = messageId;
this.manualOverride = manualOverride;
this.isReceivedWhenLocked = isReceivedWhenLocked;
Log.w(TAG, "manualOverride: " + manualOverride);
Log.w(TAG, "isReceivedWhenLocked: " + isReceivedWhenLocked);
}
public SmsDecryptJob(Context context, long messageId) {
this(context, messageId, false, false);
}
public SmsDecryptJob(Context context, long messageId, boolean isReceivedWhenLocked) {
this(context, messageId, false, isReceivedWhenLocked);
}
@Override
public void onAdded() {}
@Override
public void onRun(MasterSecret masterSecret) throws NoSuchMessageException {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
try {
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
IncomingTextMessage message = createIncomingTextMessage(masterSecret, record);
long messageId = record.getId();
long threadId = record.getThreadId();
if (message.isSecureMessage()) handleSecureMessage(masterSecret, messageId, threadId, message);
else if (message.isPreKeyBundle()) handlePreKeySignalMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message);
else if (message.isKeyExchange()) handleKeyExchangeMessage(masterSecret, messageId, threadId, (IncomingKeyExchangeMessage) message);
else if (message.isEndSession()) handleSecureMessage(masterSecret, messageId, threadId, message);
else if (message.isXmppExchange()) handleXmppExchangeMessage(masterSecret, messageId, threadId, (IncomingXmppExchangeMessage) message);
else database.updateMessageBody(masterSecret, messageId, message.getMessageBody());
if (!isReceivedWhenLocked) {
MessageNotifier.updateNotification(context, masterSecret, threadId);
} else {
MessageNotifier.updateNotification(context, masterSecret);
}
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsDecryptFailed(messageId);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
database.markAsDecryptDuplicate(messageId);
} catch (NoSessionException e) {
Log.w(TAG, e);
database.markAsNoSession(messageId);
}
}
@Override
public boolean onShouldRetryThrowable(Exception exception) {
return false;
}
@Override
public void onCanceled() {
// TODO
}
private void handleSecureMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingTextMessage message)
throws NoSessionException, DuplicateMessageException,
InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret));
IncomingTextMessage plaintext = cipher.decrypt(context, message);
database.updateMessageBody(masterSecret, messageId, plaintext.getMessageBody());
if (message.isEndSession()) SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
}
private void handlePreKeySignalMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingPreKeyBundleMessage message)
throws NoSessionException, DuplicateMessageException,
InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
try {
SmsCipher smsCipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret));
IncomingEncryptedMessage plaintext = smsCipher.decrypt(context, message);
database.updateBundleMessageBody(masterSecret, messageId, plaintext.getMessageBody());
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
} catch (InvalidVersionException e) {
Log.w(TAG, e);
database.markAsInvalidVersionKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
}
}
private void handleKeyExchangeMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingKeyExchangeMessage message)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
try {
SmsCipher cipher = new SmsCipher(new SilenceSignalProtocolStore(context, masterSecret));
OutgoingKeyExchangeMessage response = cipher.process(context, message);
if (shouldSend()) {
database.markAsProcessedKeyExchange(messageId);
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
if (response != null) {
MessageSender.send(context, masterSecret, response, threadId, true);
}
}
} catch (InvalidVersionException e) {
Log.w(TAG, e);
database.markAsInvalidVersionKeyExchange(messageId);
} catch (InvalidMessageException e) {
Log.w(TAG, e);
database.markAsCorruptKeyExchange(messageId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
database.markAsLegacyVersion(messageId);
if (shouldSend()) {
Log.w(TAG, "Legacy message found, sending updated key exchange message...");
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSender(), false);
KeyExchangeInitiator.initiate(context, masterSecret, recipients, false, message.getSubscriptionId());
database.markAsProcessedKeyExchange(messageId);
}
} catch (StaleKeyExchangeException e) {
Log.w(TAG, e);
database.markAsStaleKeyExchange(messageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
}
}
private boolean shouldSend() {
return (SilencePreferences.isAutoRespondKeyExchangeEnabled(context) || manualOverride);
}
private void handleXmppExchangeMessage(MasterSecret masterSecret, long messageId, long threadId,
IncomingXmppExchangeMessage message)
throws NoSessionException, DuplicateMessageException, InvalidMessageException, LegacyMessageException
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
database.markAsXmppExchange(messageId);
}
private String getAsymmetricDecryptedBody(MasterSecret masterSecret, String body)
throws InvalidMessageException
{
try {
AsymmetricMasterSecret asymmetricMasterSecret = MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret);
AsymmetricMasterCipher asymmetricMasterCipher = new AsymmetricMasterCipher(asymmetricMasterSecret);
return asymmetricMasterCipher.decryptBody(body);
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
private IncomingTextMessage createIncomingTextMessage(MasterSecret masterSecret, SmsMessageRecord record)
throws InvalidMessageException
{
String plaintextBody = record.getBody().getBody();
if (record.isAsymmetricEncryption()) {
plaintextBody = getAsymmetricDecryptedBody(masterSecret, record.getBody().getBody());
}
IncomingTextMessage message = new IncomingTextMessage(record.getRecipients().getPrimaryRecipient().getNumber(),
record.getRecipientDeviceId(),
record.getDateSent(),
plaintextBody);
if (record.isEndSession()) {
return new IncomingEndSessionMessage(message);
} else if (record.isBundleKeyExchange()) {
return new IncomingPreKeyBundleMessage(message, message.getMessageBody());
} else if (record.isKeyExchange()) {
return new IncomingKeyExchangeMessage(message, message.getMessageBody());
} else if (record.isXmppExchange()) {
return new IncomingXmppExchangeMessage(message, message.getMessageBody());
} else if (record.isSecure()) {
return new IncomingEncryptedMessage(message, message.getMessageBody());
}
return message;
}
}