package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.util.Log; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AsymmetricMasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.SecurityEvent; import org.thoughtcrime.securesms.crypto.SmsCipher; import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.jobqueue.JobParameters; import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.StaleKeyExchangeException; import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.messages.TextSecureGroup; import java.io.IOException; public class SmsDecryptJob extends MasterSecretJob { private static final String TAG = SmsDecryptJob.class.getSimpleName(); private final long messageId; public SmsDecryptJob(Context context, long messageId) { super(context, JobParameters.newBuilder() .withPersistence() .withRequirement(new MasterSecretRequirement(context)) .create()); this.messageId = messageId; } @Override public void onAdded() { if (KeyCachingService.getMasterSecret(context) == null) { MessageNotifier.updateNotification(context, null); } } @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, message); else if (message.isPreKeyBundle()) handlePreKeyWhisperMessage(masterSecret, messageId, threadId, (IncomingPreKeyBundleMessage) message); else if (message.isKeyExchange()) handleKeyExchangeMessage(masterSecret, messageId, threadId, (IncomingKeyExchangeMessage) message); else if (message.isEndSession()) handleSecureMessage(masterSecret, messageId, message); else database.updateMessageBody(masterSecret, messageId, message.getMessageBody()); 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, IncomingTextMessage message) throws NoSessionException, DuplicateMessageException, InvalidMessageException, LegacyMessageException { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); SmsCipher cipher = new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)); IncomingTextMessage plaintext = cipher.decrypt(context, message); database.updateMessageBody(masterSecret, messageId, plaintext.getMessageBody()); } private void handlePreKeyWhisperMessage(MasterSecret masterSecret, long messageId, long threadId, IncomingPreKeyBundleMessage message) throws NoSessionException, DuplicateMessageException, InvalidMessageException, LegacyMessageException { EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); try { SmsCipher smsCipher = new SmsCipher(new TextSecureAxolotlStore(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); if (TextSecurePreferences.isAutoRespondKeyExchangeEnabled(context)) { try { SmsCipher cipher = new SmsCipher(new TextSecureAxolotlStore(context, masterSecret)); OutgoingKeyExchangeMessage response = cipher.process(context, message); database.markAsProcessedKeyExchange(messageId); 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); } catch (StaleKeyExchangeException e) { Log.w(TAG, e); database.markAsStaleKeyExchange(messageId); } catch (UntrustedIdentityException e) { Log.w(TAG, e); } } } 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, Optional.absent()); 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.isSecure()) { return new IncomingEncryptedMessage(message, message.getMessageBody()); } return message; } }