From 728f1707b6bf435fc178ad9722f77fb448f86d0a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 8 Jan 2021 10:23:46 -0500 Subject: [PATCH] Automatically recover from bad encrypted messages. --- .../ApplicationPreferencesActivity.java | 3 + .../securesms/BindableConversationItem.java | 1 + .../conversation/ConversationFragment.java | 18 +++ .../conversation/ConversationUpdateItem.java | 10 ++ .../ConversationListItem.java | 3 +- .../securesms/crypto/SessionUtil.java | 3 + .../storage/TextSecureSessionStore.java | 10 ++ .../securesms/database/MessageDatabase.java | 1 + .../securesms/database/MmsDatabase.java | 5 + .../securesms/database/SmsDatabase.java | 32 ++++- .../database/model/MessageRecord.java | 8 +- .../database/model/SmsMessageRecord.java | 2 +- .../jobs/AutomaticSessionResetJob.java | 126 ++++++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/PushDecryptMessageJob.java | 20 +-- .../securesms/jobs/PushProcessMessageJob.java | 29 ---- .../drawable/chat_session_refresh_banner.xml | 36 +++++ app/src/main/res/drawable/ic_refresh_16.xml | 9 ++ .../res/layout/conversation_item_update.xml | 2 +- .../res/layout/decryption_failed_dialog.xml | 33 +++++ app/src/main/res/values/strings.xml | 8 +- .../api/SignalServiceMessageSender.java | 22 ++- 22 files changed, 331 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java create mode 100644 app/src/main/res/drawable/chat_session_refresh_banner.xml create mode 100644 app/src/main/res/drawable/ic_refresh_16.xml create mode 100644 app/src/main/res/layout/decryption_failed_dialog.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 6634da5c55..7eea7691a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -65,6 +65,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment"; + public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment"; @SuppressWarnings("unused") private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName(); @@ -104,6 +105,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity initFragment(android.R.id.content, new NotificationsPreferenceFragment()); } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) { initFragment(android.R.id.content, new BackupsPreferenceFragment()); + } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) { + initFragment(android.R.id.content, new HelpFragment()); } else if (icicle == null) { initFragment(android.R.id.content, new ApplicationPreferenceFragment()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index d0701824bc..9b133322db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -61,6 +61,7 @@ public interface BindableConversationItem extends Unbindable { void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position); void onVoiceNoteSeekTo(@NonNull Uri uri, double position); void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange); + void onDecryptionFailedLearnMoreClicked(); void onJoinGroupCallClicked(); void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 15048e1c68..1b9a7f82c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -66,6 +66,7 @@ import org.signal.core.util.StreamUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; @@ -1415,6 +1416,23 @@ public class ConversationFragment extends LoggingFragment { GroupsV1MigrationInfoBottomSheetDialogFragment.show(requireFragmentManager(), membershipChange); } + @Override + public void onDecryptionFailedLearnMoreClicked() { + new AlertDialog.Builder(requireContext()) + .setView(R.layout.decryption_failed_dialog) + .setPositiveButton(android.R.string.ok, (d, w) -> { + d.dismiss(); + }) + .setNeutralButton(R.string.ConversationFragment_contact_us, (d, w) -> { + Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class); + intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_HELP_FRAGMENT, true); + + startActivity(intent); + d.dismiss(); + }) + .show(); + } + @Override public void onJoinGroupCallClicked() { CommunicationActions.startVideoCall(requireActivity(), recipient.get()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 709d36a7d4..1bdd3a5f0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -206,6 +206,16 @@ public final class ConversationUpdateItem extends LinearLayout eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges()); } }); + } else if (conversationMessage.getMessageRecord().isFailedDecryptionType() && + (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType())) + { + actionButton.setText(R.string.ConversationUpdateItem_learn_more); + actionButton.setVisibility(VISIBLE); + actionButton.setOnClickListener(v -> { + if (batchSelected.isEmpty() && eventListener != null) { + eventListener.onDecryptionFailedLearnMoreClicked(); + } + }); } else if (conversationMessage.getMessageRecord().isGroupCall()) { UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true); Collection uuids = updateDescription.getMentioned(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java index 79e8e3b1f6..cdb5733e6c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -434,7 +434,8 @@ public final class ConversationListItem extends ConstraintLayout } else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) { return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint); } else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) { - return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message), defaultTint); + UpdateDescription description = UpdateDescription.staticDescription(context.getString(R.string.ThreadRecord_chat_session_refreshed), R.drawable.ic_refresh_16); + return emphasisAdded(context, description, defaultTint); } else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) { return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session), defaultTint); } else if (SmsDatabase.Types.isEndSessionType(thread.getType())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/SessionUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/SessionUtil.java index 3820742fe8..f8d125c25f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/SessionUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/SessionUtil.java @@ -29,4 +29,7 @@ public class SessionUtil { new TextSecureSessionStore(context).archiveAllSessions(); } + public static void archiveSession(Context context, RecipientId recipientId, int deviceId) { + new TextSecureSessionStore(context).archiveSession(recipientId, deviceId); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureSessionStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureSessionStore.java index 384edb306b..addf468041 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureSessionStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureSessionStore.java @@ -103,6 +103,16 @@ public class TextSecureSessionStore implements SessionStore { } } + public void archiveSession(@NonNull RecipientId recipientId, int deviceId) { + synchronized (FILE_LOCK) { + SessionRecord session = DatabaseFactory.getSessionDatabase(context).load(recipientId, deviceId); + if (session != null) { + session.archiveCurrentState(); + DatabaseFactory.getSessionDatabase(context).store(recipientId, deviceId, session); + } + } + } + public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) { synchronized (FILE_LOCK) { if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index c6e48f5dfa..ac285bd0d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -149,6 +149,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns public abstract Optional insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException; public abstract Pair insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId); public abstract Optional insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId) throws MmsException; + public abstract @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp); public abstract long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener); public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException; public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 1acdd3e9c0..6e4b8f91d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1383,6 +1383,11 @@ public class MmsDatabase extends MessageDatabase { return new Pair<>(messageId, threadId); } + @Override + public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + throw new UnsupportedOperationException(); + } + @Override public void markIncomingNotificationReceived(long threadId) { notifyConversationListeners(threadId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 212cbf9f73..01b323c4f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -1038,7 +1038,7 @@ public class SmsDatabase extends MessageDatabase { if (groupRecipient == null) threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); else threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - ContentValues values = new ContentValues(6); + ContentValues values = new ContentValues(); values.put(RECIPIENT_ID, message.getSender().serialize()); values.put(ADDRESS_DEVICE_ID, message.getSenderDeviceId()); values.put(DATE_RECEIVED, System.currentTimeMillis()); @@ -1093,6 +1093,36 @@ public class SmsDatabase extends MessageDatabase { return insertMessageInbox(message, Types.BASE_INBOX_TYPE); } + @Override + public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.resolved(recipientId)); + long type = Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT; + + type = type & (Types.TOTAL_MASK - Types.ENCRYPTION_MASK) | Types.ENCRYPTION_REMOTE_FAILED_BIT; + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(ADDRESS_DEVICE_ID, senderDeviceId); + values.put(DATE_RECEIVED, System.currentTimeMillis()); + values.put(DATE_SENT, sentTimestamp); + values.put(DATE_SERVER, -1); + values.put(READ, 0); + values.put(TYPE, type); + values.put(THREAD_ID, threadId); + + long messageId = db.insert(TABLE_NAME, null, values); + + DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1); + DatabaseFactory.getThreadDatabase(context).update(threadId, true); + + notifyConversationListeners(threadId); + + ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId)); + + return new InsertResult(messageId, threadId); + } + @Override public long insertMessageOutbox(long threadId, OutgoingTextMessage message, boolean forceSms, long date, InsertListener insertListener) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index b0aac88948..d7c9e84c35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -191,6 +191,8 @@ public abstract class MessageRecord extends DisplayRecord { else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_16); } else if (isGroupV1MigrationEvent()) { return getGroupMigrationEventDescription(context); + } else if (isFailedDecryptionType()) { + return staticUpdateDescription(context.getString(R.string.MessageRecord_chat_session_refreshed), R.drawable.ic_refresh_16); } return null; @@ -436,7 +438,7 @@ public abstract class MessageRecord extends DisplayRecord { public boolean isUpdate() { return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || - isProfileChange() || isGroupV1MigrationEvent(); + isProfileChange() || isGroupV1MigrationEvent() || isFailedDecryptionType(); } public boolean isMediaPending() { @@ -471,6 +473,10 @@ public abstract class MessageRecord extends DisplayRecord { return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure()); } + public boolean isFailedDecryptionType() { + return MmsSmsColumns.Types.isFailedDecryptType(type); + } + protected static SpannableString emphasisAdded(String sequence) { SpannableString spannable = new SpannableString(sequence); spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 0f49ba6cfa..be02a80bc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -66,7 +66,7 @@ public class SmsMessageRecord extends MessageRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { if (SmsDatabase.Types.isFailedDecryptType(type)) { - return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message)); + return emphasisAdded(context.getString(R.string.MessageRecord_chat_session_refreshed)); } else if (isCorruptedKeyExchange()) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_received_corrupted_key_exchange_message)); } else if (isInvalidVersionKeyExchange()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java new file mode 100644 index 0000000000..f5582ba3c0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -0,0 +1,126 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.SessionUtil; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.io.IOException; + +/** + * - Archives the session associated with the specified device + * - Inserts an error message in the conversation + * - Sends a new, empty message to trigger a fresh session with the specified device + * + * This will only be run when all decryptions have finished, and there can only be one enqueued + * per websocket drain cycle. + */ +public class AutomaticSessionResetJob extends BaseJob { + + private static final String TAG = Log.tag(AutomaticSessionResetJob.class); + + public static final String KEY = "AutomaticSessionResetJob"; + + private static final String KEY_RECIPIENT_ID = "recipient_id"; + private static final String KEY_DEVICE_ID = "device_id"; + private static final String KEY_SENT_TIMESTAMP = "sent_timestamp"; + + private final RecipientId recipientId; + private final int deviceId; + private final long sentTimestamp; + + public AutomaticSessionResetJob(@NonNull RecipientId recipientId, int deviceId, long sentTimestamp) { + this(new Parameters.Builder() + .setQueue(PushProcessMessageJob.getQueueName(recipientId)) + .addConstraint(DecryptionsDrainedConstraint.KEY) + .setMaxInstancesForQueue(1) + .build(), + recipientId, + deviceId, + sentTimestamp); + } + + private AutomaticSessionResetJob(@NonNull Parameters parameters, + @NonNull RecipientId recipientId, + int deviceId, + long sentTimestamp) + { + super(parameters); + this.recipientId = recipientId; + this.deviceId = deviceId; + this.sentTimestamp = sentTimestamp; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize()) + .putInt(KEY_DEVICE_ID, deviceId) + .putLong(KEY_SENT_TIMESTAMP, sentTimestamp) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + protected void onRun() throws Exception { + SessionUtil.archiveSession(context, recipientId, deviceId); + insertLocalMessage(); + sendNullMessage(); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof RetryLaterException; + } + + @Override + public void onFailure() { + } + + private void insertLocalMessage() { + MessageDatabase.InsertResult result = DatabaseFactory.getSmsDatabase(context).insertDecryptionFailedMessage(recipientId, deviceId, sentTimestamp); + ApplicationDependencies.getMessageNotifier().updateNotification(context, result.getThreadId()); + } + + private void sendNullMessage() throws IOException { + Recipient recipient = Recipient.resolved(recipientId); + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); + Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); + + try { + messageSender.sendNullMessage(address, unidentifiedAccess); + } catch (UntrustedIdentityException e) { + Log.w(TAG, "Unable to send null message."); + } + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull AutomaticSessionResetJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new AutomaticSessionResetJob(parameters, + RecipientId.from(data.getString(KEY_RECIPIENT_ID)), + data.getInt(KEY_DEVICE_ID), + data.getLong(KEY_SENT_TIMESTAMP)); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 0009dde3d5..b7b51b3f09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -65,6 +65,7 @@ public final class JobManagerFactories { put(AttachmentUploadJob.KEY, new AttachmentUploadJob.Factory()); put(AttachmentMarkUploadedJob.KEY, new AttachmentMarkUploadedJob.Factory()); put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); + put(AutomaticSessionResetJob.KEY, new AutomaticSessionResetJob.Factory()); put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java index 62f96ec151..88d337d97d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDecryptMessageJob.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -179,22 +180,11 @@ public final class PushDecryptMessageJob extends BaseJob { smsMessageId, envelope.getTimestamp())); - } catch (ProtocolInvalidMessageException | ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException e) { + } catch (ProtocolInvalidMessageException | ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); - return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.CORRUPT_MESSAGE, - toExceptionMetadata(e), - messageId, - smsMessageId, - envelope.getTimestamp())); - - } catch (ProtocolNoSessionException e) { - Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); - return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.NO_SESSION, - toExceptionMetadata(e), - messageId, - smsMessageId, - envelope.getTimestamp())); - + return Collections.singletonList(new AutomaticSessionResetJob(Recipient.external(context, e.getSender()).getId(), + e.getSenderDevice(), + envelope.getTimestamp())); } catch (ProtocolLegacyMessageException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); return Collections.singletonList(new PushProcessMessageJob(PushProcessMessageJob.MessageState.LEGACY_MESSAGE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index 00f791b9d0..8a435b1f5f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -483,16 +483,6 @@ public final class PushProcessMessageJob extends BaseJob { handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); break; - case CORRUPT_MESSAGE: - warn(TAG, String.valueOf(timestamp), "Handling corrupt message."); - handleCorruptMessage(e.sender, e.senderDevice, timestamp, smsMessageId); - break; - - case NO_SESSION: - warn(TAG, String.valueOf(timestamp), "Handling no session."); - handleNoSessionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); - break; - case LEGACY_MESSAGE: warn(TAG, String.valueOf(timestamp), "Handling legacy message."); handleLegacyMessage(e.sender, e.senderDevice, timestamp, smsMessageId); @@ -1474,23 +1464,6 @@ public final class PushProcessMessageJob extends BaseJob { } } - private void handleNoSessionMessage(@NonNull String sender, int senderDevice, long timestamp, - @NonNull Optional smsMessageId) - { - MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - - if (!smsMessageId.isPresent()) { - Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); - - if (insertResult.isPresent()) { - smsDatabase.markAsNoSession(insertResult.get().getMessageId()); - ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId()); - } - } else { - smsDatabase.markAsNoSession(smsMessageId.get()); - } - } - private void handleUnsupportedDataMessage(@NonNull String sender, int senderDevice, @NonNull Optional groupId, @@ -2049,8 +2022,6 @@ public final class PushProcessMessageJob extends BaseJob { public enum MessageState { DECRYPTED_OK, INVALID_VERSION, - CORRUPT_MESSAGE, - NO_SESSION, LEGACY_MESSAGE, DUPLICATE_MESSAGE, UNSUPPORTED_DATA_MESSAGE diff --git a/app/src/main/res/drawable/chat_session_refresh_banner.xml b/app/src/main/res/drawable/chat_session_refresh_banner.xml new file mode 100644 index 0000000000..4e97862775 --- /dev/null +++ b/app/src/main/res/drawable/chat_session_refresh_banner.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh_16.xml b/app/src/main/res/drawable/ic_refresh_16.xml new file mode 100644 index 0000000000..d9492c9098 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/conversation_item_update.xml b/app/src/main/res/layout/conversation_item_update.xml index df4d0beb0f..857d717c34 100644 --- a/app/src/main/res/layout/conversation_item_update.xml +++ b/app/src/main/res/layout/conversation_item_update.xml @@ -28,7 +28,7 @@ style="@style/Signal.Widget.Button.Small.Primary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="12dp" + android:layout_marginTop="8dp" android:visibility="gone" tools:text="Learn more" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/decryption_failed_dialog.xml b/app/src/main/res/layout/decryption_failed_dialog.xml new file mode 100644 index 0000000000..e0ca5a3414 --- /dev/null +++ b/app/src/main/res/layout/decryption_failed_dialog.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d78228afa6..d1ab518058 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,6 +356,7 @@ Tap to review Review requests carefully Signal found another contact with the same name. + Contact us There is no browser installed on your device. @@ -454,6 +455,10 @@ Today Yesterday + + Chat session refreshed + Signal uses end-to-end encryption and it may need to refresh your chat session sometimes. This doesn\'t affect your chat\'s security, but you may have missed a message from this contact, and you can ask them to resend it. + Sending Sent @@ -1080,6 +1085,7 @@ The disappearing message timer has been set to %1$s. This group was updated to a New Group. You couldn\'t be added to the New Group and have been invited to join. + Chat session refreshed A member couldn\'t be added to the New Group and has been invited to join. %1$s members couldn\'t be added to the New Group and have been invited to join. @@ -1660,6 +1666,7 @@ Contact File Video + Chat session refreshed Signal update @@ -1711,7 +1718,6 @@ %1$d:%2$02d - Bad encrypted message Message encrypted for non-existing session diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 83a0ac16c1..b2c575287f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -275,6 +275,7 @@ public class SignalServiceMessageSender { sendMessage(localAddress, Optional.absent(), timestamp, syncMessage, false, null); } + // TODO [greyson][session] Delete this when we delete the button if (message.isEndSession()) { if (recipient.getUuid().isPresent()) { store.deleteAllSessions(recipient.getUuid().get().toString()); @@ -481,7 +482,6 @@ public class SignalServiceMessageSender { attachment.getUploadTimestamp()); } - private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) throws IOException, UntrustedIdentityException { @@ -507,6 +507,26 @@ public class SignalServiceMessageSender { } } + public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional unidentifiedAccess) + throws UntrustedIdentityException, IOException + { + byte[] nullMessageBody = DataMessage.newBuilder() + .setBody(Base64.encodeBytes(Util.getRandomLengthBytes(140))) + .build() + .toByteArray(); + + NullMessage nullMessage = NullMessage.newBuilder() + .setPadding(ByteString.copyFrom(nullMessageBody)) + .build(); + + byte[] content = Content.newBuilder() + .setNullMessage(nullMessage) + .build() + .toByteArray(); + + return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null); + } + private byte[] createTypingContent(SignalServiceTypingMessage message) { Content.Builder container = Content.newBuilder(); TypingMessage.Builder builder = TypingMessage.newBuilder();