From 57c0b8fd0f896206f5c09eb78df751ac03ef607c Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 14 May 2021 14:03:35 -0400 Subject: [PATCH] Initial pre-alpha support for sender key. --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 2 + .../securesms/AppCapabilities.java | 4 +- .../securesms/ApplicationContext.java | 5 + .../securesms/BindableConversationItem.java | 3 +- .../securesms/backup/FullBackupExporter.java | 9 +- .../app/internal/InternalSettingsFragment.kt | 41 ++ .../app/internal/InternalSettingsState.kt | 3 +- .../app/internal/InternalSettingsViewModel.kt | 8 +- .../BadDecryptLearnMoreDialog.java | 68 ++ .../conversation/ConversationFragment.java | 9 +- .../conversation/ConversationUpdateItem.java | 16 +- .../securesms/conversation/MenuState.java | 18 +- .../ConversationListItem.java | 15 +- .../securesms/crypto/SenderKeyUtil.java | 43 ++ .../crypto/UnidentifiedAccessUtil.java | 18 + .../storage/SignalProtocolStoreImpl.java | 30 +- .../crypto/storage/SignalSenderKeyStore.java | 88 +++ .../storage/TextSecureIdentityKeyStore.java | 2 + .../storage/TextSecureSessionStore.java | 22 + .../securesms/database/DatabaseFactory.java | 119 ++-- .../securesms/database/GroupDatabase.java | 84 ++- .../database/GroupReceiptDatabase.java | 21 + .../securesms/database/MessageDatabase.java | 3 +- .../securesms/database/MmsDatabase.java | 8 +- .../securesms/database/MmsSmsColumns.java | 7 +- .../database/PendingRetryReceiptDatabase.java | 89 +++ .../securesms/database/RecipientDatabase.java | 18 +- .../securesms/database/SenderKeyDatabase.java | 120 ++++ .../database/SenderKeySharedDatabase.java | 141 +++++ .../securesms/database/SessionDatabase.java | 50 +- .../securesms/database/SmsDatabase.java | 24 +- .../database/helpers/SQLCipherOpenHelper.java | 48 +- .../database/model/MediaMmsMessageRecord.java | 2 +- .../database/model/MessageRecord.java | 17 +- .../model/PendingRetryReceiptModel.kt | 13 + .../database/model/SmsMessageRecord.java | 2 +- .../dependencies/ApplicationDependencies.java | 16 + .../ApplicationDependencyProvider.java | 6 + .../jobs/AutomaticSessionResetJob.java | 5 +- .../jobs/GroupCallUpdateSendJob.java | 3 +- .../securesms/jobs/JobManagerFactories.java | 2 + .../securesms/jobs/LeaveGroupJob.java | 3 +- .../jobs/MultiDeviceBlockedUpdateJob.java | 4 +- .../MultiDeviceConfigurationUpdateJob.java | 8 +- .../jobs/MultiDeviceContactUpdateJob.java | 4 +- .../jobs/MultiDeviceGroupUpdateJob.java | 5 +- .../jobs/MultiDeviceKeysUpdateJob.java | 4 +- .../MultiDeviceMessageRequestResponseJob.java | 4 +- .../MultiDeviceOutgoingPaymentSyncJob.java | 4 +- .../MultiDeviceProfileContentUpdateJob.java | 4 +- .../jobs/MultiDeviceProfileKeyUpdateJob.java | 2 +- .../jobs/MultiDeviceReadUpdateJob.java | 2 +- .../MultiDeviceStickerPackOperationJob.java | 4 +- .../jobs/MultiDeviceStickerPackSyncJob.java | 4 +- .../MultiDeviceStorageSyncRequestJob.java | 4 +- .../jobs/MultiDeviceVerifiedUpdateJob.java | 4 +- .../jobs/MultiDeviceViewOnceOpenJob.java | 2 +- .../jobs/MultiDeviceViewedUpdateJob.java | 2 +- .../jobs/PaymentNotificationSendJob.java | 5 +- .../securesms/jobs/ProfileKeySendJob.java | 3 +- .../securesms/jobs/PushGroupSendJob.java | 14 +- .../jobs/PushGroupSilentUpdateSendJob.java | 23 +- .../securesms/jobs/PushGroupUpdateJob.java | 8 +- .../securesms/jobs/PushMediaSendJob.java | 5 +- .../securesms/jobs/PushTextSendJob.java | 5 +- .../securesms/jobs/ReactionSendJob.java | 24 +- .../securesms/jobs/RemoteDeleteSendJob.java | 23 +- .../securesms/jobs/RequestGroupInfoJob.java | 8 +- .../securesms/jobs/SendRetryReceiptJob.java | 119 ++++ .../jobs/SenderKeyDistributionSendJob.java | 117 ++++ .../securesms/jobs/TypingSendJob.java | 41 +- .../securesms/keyvalue/InternalValues.java | 10 +- .../logsubmit/LogSectionCapabilities.java | 4 +- .../securesms/messages/GroupSendUtil.java | 319 ++++++++++ .../messages/IncomingMessageProcessor.java | 2 +- .../messages/MessageContentProcessor.java | 96 +++ .../messages/MessageDecryptionUtil.java | 76 ++- .../migrations/ApplicationMigrations.java | 7 +- .../securesms/recipients/Recipient.java | 14 + .../recipients/RecipientDetails.java | 3 + .../service/CodeVerificationRequest.java | 2 + .../service/PendingRetryReceiptManager.java | 88 +++ .../securesms/util/FeatureFlags.java | 45 +- .../res/drawable/chat_session_refresh.xml | 39 ++ ...bad_decrypt_learn_more_dialog_fragment.xml | 34 + app/src/main/res/values/strings.xml | 14 + app/witness-verifications.gradle | 35 +- device-transfer/lib/build.gradle | 2 +- .../lib/witness-verifications.gradle | 4 +- libsignal/service/build.gradle | 2 +- .../api/SignalServiceMessagePipe.java | 29 + .../api/SignalServiceMessageSender.java | 581 ++++++++++++++---- .../api/SignalServiceProtocolStore.java | 2 +- .../api/SignalServiceSenderKeyStore.java | 29 + .../api/account/AccountAttributes.java | 10 +- .../signalservice/api/crypto/ContentHint.java | 38 ++ .../api/crypto/EnvelopeContent.java | 156 +++++ .../api/crypto/SignalGroupCipher.java | 39 ++ .../api/crypto/SignalGroupSessionBuilder.java | 35 ++ .../api/crypto/SignalSealedSessionCipher.java | 19 +- .../api/crypto/SignalServiceCipher.java | 68 +- .../api/messages/SendMessageResult.java | 4 + .../api/messages/SignalServiceContent.java | 218 ++++++- .../messages/SignalServiceDataMessage.java | 15 + .../api/messages/SignalServiceEnvelope.java | 4 + .../api/messages/SignalServiceMetadata.java | 10 +- .../api/profiles/SignalServiceProfile.java | 6 + .../api/push/DistributionId.java | 41 ++ .../NonSuccessfulResponseCodeException.java | 3 +- .../internal/push/GroupMismatchedDevices.java | 24 + .../internal/push/GroupStaleDevices.java | 23 + .../internal/push/PushServiceSocket.java | 62 ++ .../push/SendGroupMessageResponse.java | 31 + .../GroupMismatchedDevicesException.java | 24 + .../GroupStaleDevicesException.java | 24 + ...alidUnidentifiedAccessHeaderException.java | 14 + ...gnalServiceMetadataProtobufSerializer.java | 29 +- .../main/proto/InternalSerialization.proto | 1 + .../src/main/proto/SignalService.proto | 22 +- .../api/account/AccountAttributesTest.java | 12 +- .../push/GroupMismatchedDevicesTest.java | 40 ++ .../internal/push/GroupStaleDevicesTest.java | 36 ++ .../service/witness-verifications.gradle | 4 +- 124 files changed, 3668 insertions(+), 444 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/BadDecryptLearnMoreDialog.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/SenderKeyUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalSenderKeyStore.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptDatabase.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/SenderKeyDatabase.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/SenderKeySharedDatabase.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/PendingRetryReceiptModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/service/PendingRetryReceiptManager.java create mode 100644 app/src/main/res/drawable/chat_session_refresh.xml create mode 100644 app/src/main/res/layout/bad_decrypt_learn_more_dialog_fragment.xml create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceSenderKeyStore.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ContentHint.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeContent.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupCipher.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupSessionBuilder.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/DistributionId.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevices.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupStaleDevices.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupMismatchedDevicesException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupStaleDevicesException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/InvalidUnidentifiedAccessHeaderException.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevicesTest.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupStaleDevicesTest.java diff --git a/app/build.gradle b/app/build.gradle index 244c2e2f4d..61e7624514 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -376,7 +376,7 @@ dependencies { implementation project(':device-transfer') implementation 'org.signal:zkgroup-android:0.7.0' - implementation 'org.whispersystems:signal-client-android:0.5.1' + implementation 'org.whispersystems:signal-client-android:0.8.0' implementation 'com.google.protobuf:protobuf-javalite:3.10.0' implementation('com.mobilecoin:android-sdk:1.0.0') { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a3942c75e..0291bd8aaa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -736,6 +736,8 @@ + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java index 83d9c8f08e..6bb9f9de3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms; -import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.account.AccountAttributes; public final class AppCapabilities { @@ -11,12 +10,13 @@ public final class AppCapabilities { private static final boolean UUID_CAPABLE = false; private static final boolean GV2_CAPABLE = true; private static final boolean GV1_MIGRATION = true; + private static final boolean SENDER_KEY = true; /** * @param storageCapable Whether or not the user can use storage service. This is another way of * asking if the user has set a Signal PIN or not. */ public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { - return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION); + return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index cd71342710..c207450264 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -146,6 +146,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addBlocking("blob-provider", this::initializeBlobProvider) .addBlocking("feature-flags", FeatureFlags::init) .addNonBlocking(this::initializeRevealableMessageManager) + .addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializeGcmCheck) .addNonBlocking(this::initializeSignedPreKeyCheck) .addNonBlocking(this::initializePeriodicTasks) @@ -300,6 +301,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary(); } + private void initializePendingRetryReceiptManager() { + ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary(); + } + private void initializePeriodicTasks() { RotateSignedPreKeyListener.schedule(this); DirectoryRefreshListener.schedule(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 6474a2fa54..f4c7c002dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -72,7 +72,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position); void onVoiceNoteSeekTo(@NonNull Uri uri, double position); void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange); - void onDecryptionFailedLearnMoreClicked(); + void onChatSessionRefreshLearnMoreClicked(); + void onBadDecryptLearnMoreClicked(@NonNull RecipientId author); void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient); void onJoinGroupCallClicked(); void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java index 75ced30c75..941373c456 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -30,7 +30,10 @@ import org.thoughtcrime.securesms.database.KeyValueDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; +import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; +import org.thoughtcrime.securesms.database.SenderKeyDatabase; +import org.thoughtcrime.securesms.database.SenderKeySharedDatabase; import org.thoughtcrime.securesms.database.SessionDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; @@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.profiles.AvatarHelper; +import org.thoughtcrime.securesms.service.PendingRetryReceiptManager; import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -77,7 +81,10 @@ public class FullBackupExporter extends FullBackupBase { SessionDatabase.TABLE_NAME, SearchDatabase.SMS_FTS_TABLE_NAME, SearchDatabase.MMS_FTS_TABLE_NAME, - EmojiSearchDatabase.TABLE_NAME + EmojiSearchDatabase.TABLE_NAME, + SenderKeyDatabase.TABLE_NAME, + SenderKeySharedDatabase.TABLE_NAME, + PendingRetryReceiptDatabase.TABLE_NAME ); public static void export(@NonNull Context context, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index ae4999571b..fb96d52dc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.configure +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob @@ -212,6 +213,35 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter viewModel.setDisableAutoMigrationNotification(!state.useBuiltInEmojiSet) } ) + + dividerPref() + + sectionHeaderPref(R.string.preferences__internal_sender_key) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_clear_all_state), + summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sender_key_state), + onClick = { + clearAllSenderKeyState() + } + ) + + clickPref( + title = DSLSettingsText.from(R.string.preferences__internal_clear_shared_state), + summary = DSLSettingsText.from(R.string.preferences__internal_click_to_delete_all_sharing_state), + onClick = { + clearAllSenderKeySharedState() + } + ) + + switchPref( + title = DSLSettingsText.from(R.string.preferences__internal_remove_two_person_minimum), + summary = DSLSettingsText.from(R.string.preferences__internal_remove_the_requirement_that_you_need), + isChecked = state.removeSenderKeyMinimium, + onClick = { + viewModel.setRemoveSenderKeyMinimum(!state.removeSenderKeyMinimium) + } + ) } } @@ -278,4 +308,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter ConversationUtil.clearAllShortcuts(requireContext()) Toast.makeText(context, "Deleted all dynamic shortcuts.", Toast.LENGTH_SHORT).show() } + + private fun clearAllSenderKeyState() { + DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll() + DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll() + Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show() + } + + private fun clearAllSenderKeySharedState() { + DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll() + Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt index 036c9bbaea..777dfdb13a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt @@ -12,5 +12,6 @@ data class InternalSettingsState( val disableAutoMigrationNotification: Boolean, val forceCensorship: Boolean, val useBuiltInEmojiSet: Boolean, - val emojiVersion: EmojiFiles.Version? + val emojiVersion: EmojiFiles.Version?, + val removeSenderKeyMinimium: Boolean, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index b42fc010ee..12980bd4df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -65,6 +65,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito refresh() } + fun setRemoveSenderKeyMinimum(enabled: Boolean) { + preferenceDataStore.putBoolean(InternalValues.REMOVE_SENDER_KEY_MINIMUM, enabled) + refresh() + } + private fun refresh() { store.update { getState().copy(emojiVersion = it.emojiVersion) } } @@ -79,7 +84,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(), forceCensorship = SignalStore.internalValues().forcedCensorship(), useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(), - emojiVersion = null + emojiVersion = null, + removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum() ) class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/BadDecryptLearnMoreDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/BadDecryptLearnMoreDialog.java new file mode 100644 index 0000000000..7cd214320d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/BadDecryptLearnMoreDialog.java @@ -0,0 +1,68 @@ +package org.thoughtcrime.securesms.conversation; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; + +/** + * A dialog fragment that shows when you click 'learn more' on a {@link MessageRecord#isBadDecryptType()}. + */ +public final class BadDecryptLearnMoreDialog extends DialogFragment { + + private static final String TAG = Log.tag(BadDecryptLearnMoreDialog.class); + private static final String FRAGMENT_TAG = "BadDecryptLearnMoreDialog"; + + private static final String KEY_DISPLAY_NAME = "display_name"; + private static final String KEY_GROUP_CHAT = "group_chat"; + + public static void show(@NonNull FragmentManager fragmentManager, @NonNull String displayName, boolean isGroupChat) { + if (fragmentManager.findFragmentByTag(FRAGMENT_TAG) != null) { + Log.i(TAG, "Already shown!"); + return; + } + + Bundle args = new Bundle(); + args.putString(KEY_DISPLAY_NAME, displayName); + args.putBoolean(KEY_GROUP_CHAT, isGroupChat); + + BadDecryptLearnMoreDialog fragment = new BadDecryptLearnMoreDialog(); + fragment.setArguments(args); + fragment.show(fragmentManager, FRAGMENT_TAG); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(requireContext()); + + View view = LayoutInflater.from(requireContext()).inflate(R.layout.bad_decrypt_learn_more_dialog_fragment, null); + TextView body = view.findViewById(R.id.bad_decrypt_dialog_body); + + String displayName = requireArguments().getString(KEY_DISPLAY_NAME); + boolean isGroup = requireArguments().getBoolean(KEY_GROUP_CHAT); + + if (isGroup) { + body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_group, displayName)); + } else { + body.setText(getString(R.string.BadDecryptLearnMoreDialog_couldnt_be_delivered_individual, displayName)); + } + + dialogBuilder.setView(view) + .setPositiveButton(android.R.string.ok, null); + + return dialogBuilder.create(); + } +} 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 2b2d09c0ef..afa90d91e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -1605,7 +1605,7 @@ public class ConversationFragment extends LoggingFragment { } @Override - public void onDecryptionFailedLearnMoreClicked() { + public void onChatSessionRefreshLearnMoreClicked() { new AlertDialog.Builder(requireContext()) .setView(R.layout.decryption_failed_dialog) .setPositiveButton(android.R.string.ok, (d, w) -> { @@ -1618,6 +1618,13 @@ public class ConversationFragment extends LoggingFragment { .show(); } + @Override + public void onBadDecryptLearnMoreClicked(@NonNull RecipientId author) { + SimpleTask.run(getLifecycle(), + () -> Recipient.resolved(author).getDisplayName(requireContext()), + name -> BadDecryptLearnMoreDialog.show(getParentFragmentManager(), name, recipient.get().isGroup())); + } + @Override public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) { if (recipient.isGroup()) { 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 2ef6d9b6d6..f21c4d499f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -292,14 +292,14 @@ public final class ConversationUpdateItem extends FrameLayout eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges()); } }); - } else if (conversationMessage.getMessageRecord().isFailedDecryptionType() && - (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType())) + } else if (conversationMessage.getMessageRecord().isChatSessionRefresh() && + (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isChatSessionRefresh())) { actionButton.setText(R.string.ConversationUpdateItem_learn_more); actionButton.setVisibility(VISIBLE); actionButton.setOnClickListener(v -> { if (batchSelected.isEmpty() && eventListener != null) { - eventListener.onDecryptionFailedLearnMoreClicked(); + eventListener.onChatSessionRefreshLearnMoreClicked(); } }); } else if (conversationMessage.getMessageRecord().isIdentityUpdate()) { @@ -370,6 +370,16 @@ public final class ConversationUpdateItem extends FrameLayout eventListener.onViewGroupDescriptionChange(conversationRecipient.getGroupId().orNull(), conversationMessage.getMessageRecord().getGroupV2DescriptionUpdate(), isMessageRequestAccepted); } }); + } else if (conversationMessage.getMessageRecord().isBadDecryptType() && + (!nextMessageRecord.isPresent() || !nextMessageRecord.get().isBadDecryptType())) + { + actionButton.setText(R.string.ConversationUpdateItem_learn_more); + actionButton.setVisibility(VISIBLE); + actionButton.setOnClickListener(v -> { + if (batchSelected.isEmpty() && eventListener != null) { + eventListener.onBadDecryptLearnMoreClicked(conversationMessage.getMessageRecord().getRecipient().getId()); + } + }); } else { actionButton.setVisibility(GONE); actionButton.setOnClickListener(null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java index 7bd9eaed89..cdb2ec0736 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -134,17 +134,17 @@ final class MenuState { } static boolean isActionMessage(@NonNull MessageRecord messageRecord) { - return messageRecord.isGroupAction() || - messageRecord.isCallLog() || - messageRecord.isJoined() || + return messageRecord.isGroupAction() || + messageRecord.isCallLog() || + messageRecord.isJoined() || messageRecord.isExpirationTimerUpdate() || - messageRecord.isEndSession() || - messageRecord.isIdentityUpdate() || - messageRecord.isIdentityVerified() || - messageRecord.isIdentityDefault() || - messageRecord.isProfileChange() || + messageRecord.isEndSession() || + messageRecord.isIdentityUpdate() || + messageRecord.isIdentityVerified() || + messageRecord.isIdentityDefault() || + messageRecord.isProfileChange() || messageRecord.isGroupV1MigrationEvent() || - messageRecord.isFailedDecryptionType() || + messageRecord.isChatSessionRefresh() || messageRecord.isInMemoryMessageRecord(); } 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 7623694c56..7fefabb872 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java @@ -354,10 +354,13 @@ public final class ConversationListItem extends ConstraintLayout } private void setStatusIcons(ThreadRecord thread) { - if (!thread.isOutgoing() || - thread.isOutgoingAudioCall() || - thread.isOutgoingVideoCall() || - thread.isVerificationStatusChange()) + if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) { + deliveryStatusIndicator.setNone(); + alertView.setFailed(); + } else if (!thread.isOutgoing() || + thread.isOutgoingAudioCall() || + thread.isOutgoingVideoCall() || + thread.isVerificationStatusChange()) { deliveryStatusIndicator.setNone(); alertView.setNone(); @@ -435,7 +438,7 @@ public final class ConversationListItem extends ConstraintLayout return emphasisAdded(context, context.getString(R.string.ThreadRecord_left_the_group), defaultTint); } 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())) { + } else if (SmsDatabase.Types.isChatSessionRefresh(thread.getType())) { 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())) { @@ -482,6 +485,8 @@ public final class ConversationListItem extends ConstraintLayout return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint); } else if (SmsDatabase.Types.isProfileChange(thread.getType())) { return emphasisAdded(context, "", defaultTint); + } else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) { + return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint); } else { ThreadDatabase.Extra extra = thread.getExtra(); if (extra != null && extra.isViewOnce()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/SenderKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/SenderKeyUtil.java new file mode 100644 index 0000000000..40dc2a6bae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/SenderKeyUtil.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.crypto; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.storage.SignalSenderKeyStore; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.whispersystems.signalservice.api.SignalSessionLock; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public final class SenderKeyUtil { + private SenderKeyUtil() {} + + /** + * Clears the state for a sender key session we created. It will naturally get re-created when it is next needed, rotating the key. + */ + public static void rotateOurKey(@NonNull Context context, @NonNull DistributionId distributionId) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + new SignalSenderKeyStore(context).deleteAllFor(Recipient.self().getId(), distributionId); + DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(distributionId); + } + } + + /** + * Gets when the sender key session was created, or -1 if it doesn't exist. + */ + public static long getCreateTimeForOurKey(@NonNull Context context, @NonNull DistributionId distributionId) { + return DatabaseFactory.getSenderKeyDatabase(context).getCreatedTime(Recipient.self().getId(), SignalServiceAddress.DEFAULT_DEVICE_ID, distributionId); + } + + /** + * Deletes all stored state around session keys. Should only really be used when the user is re-registering. + */ + public static void clearAllState(@NonNull Context context) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + new SignalSenderKeyStore(context).deleteAll(); + DatabaseFactory.getSenderKeySharedDatabase(context).deleteAll(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java index 014328fc1a..8f27f614af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/UnidentifiedAccessUtil.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.keyvalue.CertificateType; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -32,6 +33,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -63,6 +65,22 @@ public class UnidentifiedAccessUtil { return getAccessFor(context, recipients, true); } + @WorkerThread + public static Map> getAccessMapFor(@NonNull Context context, @NonNull List recipients) { + List> accessList = getAccessFor(context, recipients, true); + + Iterator recipientIterator = recipients.iterator(); + Iterator> accessIterator = accessList.iterator(); + + Map> accessMap = new HashMap<>(recipients.size()); + + while (recipientIterator.hasNext()) { + accessMap.put(recipientIterator.next().getId(), accessIterator.next()); + } + + return accessMap; + } + @WorkerThread public static List> getAccessFor(@NonNull Context context, @NonNull List recipients, boolean log) { byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java index 0ebbb67a07..5276f5d643 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalProtocolStoreImpl.java @@ -5,6 +5,7 @@ import android.content.Context; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.groups.state.SenderKeyRecord; import org.whispersystems.libsignal.state.IdentityKeyStore; @@ -17,8 +18,11 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.signalservice.api.SignalServiceProtocolStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore; +import org.whispersystems.signalservice.api.push.DistributionId; +import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.UUID; public class SignalProtocolStoreImpl implements SignalServiceProtocolStore { @@ -27,12 +31,14 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore { private final SignedPreKeyStore signedPreKeyStore; private final IdentityKeyStore identityKeyStore; private final SignalServiceSessionStore sessionStore; + private final SignalSenderKeyStore senderKeyStore; public SignalProtocolStoreImpl(Context context) { this.preKeyStore = new TextSecurePreKeyStore(context); this.signedPreKeyStore = new TextSecurePreKeyStore(context); this.identityKeyStore = new TextSecureIdentityKeyStore(context); this.sessionStore = new TextSecureSessionStore(context); + this.senderKeyStore = new SignalSenderKeyStore(context); } @Override @@ -85,6 +91,11 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore { return sessionStore.loadSession(axolotlAddress); } + @Override + public List loadExistingSessions(List addresses) throws NoSessionException { + return sessionStore.loadExistingSessions(addresses); + } + @Override public List getSubDeviceSessions(String number) { return sessionStore.getSubDeviceSessions(number); @@ -142,11 +153,26 @@ public class SignalProtocolStoreImpl implements SignalServiceProtocolStore { @Override public void storeSenderKey(SignalProtocolAddress sender, UUID distributionId, SenderKeyRecord record) { - + senderKeyStore.storeSenderKey(sender, distributionId, record); } @Override public SenderKeyRecord loadSenderKey(SignalProtocolAddress sender, UUID distributionId) { - return null; + return senderKeyStore.loadSenderKey(sender, distributionId); + } + + @Override + public Set getSenderKeySharedWith(DistributionId distributionId) { + return senderKeyStore.getSenderKeySharedWith(distributionId); + } + + @Override + public void markSenderKeySharedWith(DistributionId distributionId, Collection addresses) { + senderKeyStore.markSenderKeySharedWith(distributionId, addresses); + } + + @Override + public void clearSenderKeySharedWith(DistributionId distributionId, Collection addresses) { + senderKeyStore.clearSenderKeySharedWith(distributionId, addresses); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalSenderKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalSenderKeyStore.java new file mode 100644 index 0000000000..6a70267a8c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/SignalSenderKeyStore.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.crypto.storage; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.crypto.DatabaseSessionLock; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.util.Collection; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Nullable; + +/** + * An implementation of the storage interface used by the protocol layer to store sender keys. For + * more details around sender keys, see {@link org.thoughtcrime.securesms.database.SenderKeyDatabase}. + */ +public final class SignalSenderKeyStore implements SignalServiceSenderKeyStore { + + private final Context context; + + public SignalSenderKeyStore(@NonNull Context context) { + this.context = context; + } + + @Override + public void storeSenderKey(@NonNull SignalProtocolAddress sender, @NonNull UUID distributionId, @NonNull SenderKeyRecord record) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + RecipientId recipientId = RecipientId.fromExternalPush(sender.getName()); + DatabaseFactory.getSenderKeyDatabase(context).store(recipientId, sender.getDeviceId(), DistributionId.from(distributionId), record); + } + } + + @Override + public @Nullable SenderKeyRecord loadSenderKey(@NonNull SignalProtocolAddress sender, @NonNull UUID distributionId) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + RecipientId recipientId = RecipientId.fromExternalPush(sender.getName()); + return DatabaseFactory.getSenderKeyDatabase(context).load(recipientId, sender.getDeviceId(), DistributionId.from(distributionId)); + } + } + + @Override + public Set getSenderKeySharedWith(DistributionId distributionId) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + return DatabaseFactory.getSenderKeySharedDatabase(context).getSharedWith(distributionId); + } + } + + @Override + public void markSenderKeySharedWith(DistributionId distributionId, Collection addresses) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + DatabaseFactory.getSenderKeySharedDatabase(context).markAsShared(distributionId, addresses); + } + } + + @Override + public void clearSenderKeySharedWith(DistributionId distributionId, Collection addresses) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, addresses); + } + } + + /** + * Removes all sender key session state for all devices for the provided recipient-distributionId pair. + */ + public void deleteAllFor(@NonNull RecipientId recipientId, @NonNull DistributionId distributionId) { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + DatabaseFactory.getSenderKeyDatabase(context).deleteAllFor(recipientId, distributionId); + } + } + + /** + * Deletes all sender key session state. + */ + public void deleteAll() { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + DatabaseFactory.getSenderKeyDatabase(context).deleteAll(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java index 619955fed9..25ae6788c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/storage/TextSecureIdentityKeyStore.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus; +import org.thoughtcrime.securesms.database.SenderKeySharedDatabase; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.IdentityUtil; @@ -73,6 +74,7 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore { identityDatabase.saveIdentity(recipientId, identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval); IdentityUtil.markIdentityUpdate(context, recipientId); SessionUtil.archiveSiblingSessions(context, address); + DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId); return true; } 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 75a08cdadb..4d52b0591b 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 @@ -8,8 +8,10 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.DatabaseSessionLock; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SessionDatabase; +import org.thoughtcrime.securesms.database.SessionDatabase.RecipientDevice; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.protocol.CiphertextMessage; import org.whispersystems.libsignal.state.SessionRecord; @@ -18,6 +20,7 @@ import org.whispersystems.signalservice.api.SignalSessionLock; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; public class TextSecureSessionStore implements SignalServiceSessionStore { @@ -44,6 +47,25 @@ public class TextSecureSessionStore implements SignalServiceSessionStore { } } + @Override + public List loadExistingSessions(List addresses) throws NoSessionException { + try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { + List ids = addresses.stream() + .map(address -> new RecipientDevice(RecipientId.fromExternalPush(address.getName()), address.getDeviceId())) + .collect(Collectors.toList()); + + List sessionRecords = DatabaseFactory.getSessionDatabase(context).load(ids); + + if (sessionRecords.size() != addresses.size()) { + String message = "Mismatch! Asked for " + addresses.size() + " sessions, but only found " + sessionRecords.size() + "!"; + Log.w(TAG, message); + throw new NoSessionException(message); + } + + return sessionRecords; + } + } + @Override public void storeSession(@NonNull SignalProtocolAddress address, @NonNull SessionRecord record) { try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index 5295f27d1f..302e1c49ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -41,31 +42,34 @@ public class DatabaseFactory { private static volatile DatabaseFactory instance; - private final SQLCipherOpenHelper databaseHelper; - private final SmsDatabase sms; - private final MmsDatabase mms; - private final AttachmentDatabase attachments; - private final MediaDatabase media; - private final ThreadDatabase thread; - private final MmsSmsDatabase mmsSmsDatabase; - private final IdentityDatabase identityDatabase; - private final DraftDatabase draftDatabase; - private final PushDatabase pushDatabase; - private final GroupDatabase groupDatabase; - private final RecipientDatabase recipientDatabase; - private final ContactsDatabase contactsDatabase; - private final GroupReceiptDatabase groupReceiptDatabase; - private final OneTimePreKeyDatabase preKeyDatabase; - private final SignedPreKeyDatabase signedPreKeyDatabase; - private final SessionDatabase sessionDatabase; - private final SearchDatabase searchDatabase; - private final StickerDatabase stickerDatabase; - private final UnknownStorageIdDatabase storageIdDatabase; - private final RemappedRecordsDatabase remappedRecordsDatabase; - private final MentionDatabase mentionDatabase; - private final PaymentDatabase paymentDatabase; - private final ChatColorsDatabase chatColorsDatabase; - private final EmojiSearchDatabase emojiSearchDatabase; + private final SQLCipherOpenHelper databaseHelper; + private final SmsDatabase sms; + private final MmsDatabase mms; + private final AttachmentDatabase attachments; + private final MediaDatabase media; + private final ThreadDatabase thread; + private final MmsSmsDatabase mmsSmsDatabase; + private final IdentityDatabase identityDatabase; + private final DraftDatabase draftDatabase; + private final PushDatabase pushDatabase; + private final GroupDatabase groupDatabase; + private final RecipientDatabase recipientDatabase; + private final ContactsDatabase contactsDatabase; + private final GroupReceiptDatabase groupReceiptDatabase; + private final OneTimePreKeyDatabase preKeyDatabase; + private final SignedPreKeyDatabase signedPreKeyDatabase; + private final SessionDatabase sessionDatabase; + private final SenderKeyDatabase senderKeyDatabase; + private final SenderKeySharedDatabase senderKeySharedDatabase; + private final PendingRetryReceiptDatabase pendingRetryReceiptDatabase; + private final SearchDatabase searchDatabase; + private final StickerDatabase stickerDatabase; + private final UnknownStorageIdDatabase storageIdDatabase; + private final RemappedRecordsDatabase remappedRecordsDatabase; + private final MentionDatabase mentionDatabase; + private final PaymentDatabase paymentDatabase; + private final ChatColorsDatabase chatColorsDatabase; + private final EmojiSearchDatabase emojiSearchDatabase; public static DatabaseFactory getInstance(Context context) { if (instance == null) { @@ -148,6 +152,18 @@ public class DatabaseFactory { return getInstance(context).sessionDatabase; } + public static SenderKeyDatabase getSenderKeyDatabase(Context context) { + return getInstance(context).senderKeyDatabase; + } + + public static SenderKeySharedDatabase getSenderKeySharedDatabase(Context context) { + return getInstance(context).senderKeySharedDatabase; + } + + public static PendingRetryReceiptDatabase getPendingRetryReceiptDatabase(Context context) { + return getInstance(context).pendingRetryReceiptDatabase; + } + public static SearchDatabase getSearchDatabase(Context context) { return getInstance(context).searchDatabase; } @@ -210,31 +226,34 @@ public class DatabaseFactory { DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context); AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); - this.sms = new SmsDatabase(context, databaseHelper); - this.mms = new MmsDatabase(context, databaseHelper); - this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); - this.media = new MediaDatabase(context, databaseHelper); - this.thread = new ThreadDatabase(context, databaseHelper); - this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); - this.identityDatabase = new IdentityDatabase(context, databaseHelper); - this.draftDatabase = new DraftDatabase(context, databaseHelper); - this.pushDatabase = new PushDatabase(context, databaseHelper); - this.groupDatabase = new GroupDatabase(context, databaseHelper); - this.recipientDatabase = new RecipientDatabase(context, databaseHelper); - this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); - this.contactsDatabase = new ContactsDatabase(context); - this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); - this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); - this.sessionDatabase = new SessionDatabase(context, databaseHelper); - this.searchDatabase = new SearchDatabase(context, databaseHelper); - this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); - this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper); - this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper); - this.mentionDatabase = new MentionDatabase(context, databaseHelper); - this.paymentDatabase = new PaymentDatabase(context, databaseHelper); - this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper); - this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper); + this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); + this.sms = new SmsDatabase(context, databaseHelper); + this.mms = new MmsDatabase(context, databaseHelper); + this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); + this.media = new MediaDatabase(context, databaseHelper); + this.thread = new ThreadDatabase(context, databaseHelper); + this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); + this.identityDatabase = new IdentityDatabase(context, databaseHelper); + this.draftDatabase = new DraftDatabase(context, databaseHelper); + this.pushDatabase = new PushDatabase(context, databaseHelper); + this.groupDatabase = new GroupDatabase(context, databaseHelper); + this.recipientDatabase = new RecipientDatabase(context, databaseHelper); + this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); + this.contactsDatabase = new ContactsDatabase(context); + this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); + this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); + this.sessionDatabase = new SessionDatabase(context, databaseHelper); + this.senderKeyDatabase = new SenderKeyDatabase(context, databaseHelper); + this.senderKeySharedDatabase = new SenderKeySharedDatabase(context, databaseHelper); + this.pendingRetryReceiptDatabase = new PendingRetryReceiptDatabase(context, databaseHelper); + this.searchDatabase = new SearchDatabase(context, databaseHelper); + this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); + this.storageIdDatabase = new UnknownStorageIdDatabase(context, databaseHelper); + this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper); + this.mentionDatabase = new MentionDatabase(context, databaseHelper); + this.paymentDatabase = new PaymentDatabase(context, databaseHelper); + this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper); + this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 6b0b993d7e..10a74d2903 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -20,7 +20,9 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; +import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.whispersystems.signalservice.api.push.DistributionId; import org.thoughtcrime.securesms.groups.GroupAccessControl; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; @@ -48,6 +50,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -71,6 +74,7 @@ public final class GroupDatabase extends Database { static final String MMS = "mms"; private static final String EXPECTED_V2_ID = "expected_v2_id"; private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members"; + private static final String DISTRIBUTION_ID = "distribution_id"; /* V2 Group columns */ @@ -98,15 +102,17 @@ public final class GroupDatabase extends Database { V2_REVISION + " BLOB, " + V2_DECRYPTED_GROUP + " BLOB, " + EXPECTED_V2_ID + " TEXT DEFAULT NULL, " + - UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL);"; + UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL, " + + DISTRIBUTION_ID + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");", "CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");", - "CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");" - }; + "CREATE UNIQUE INDEX IF NOT EXISTS expected_v2_id_index ON " + TABLE_NAME + " (" + EXPECTED_V2_ID + ");", + "CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON " + TABLE_NAME + "(" + DISTRIBUTION_ID + ")" +}; - private static final String[] GROUP_PROJECTION = { +private static final String[] GROUP_PROJECTION = { GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST, TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP }; @@ -256,6 +262,38 @@ public final class GroupDatabase extends Database { return new Reader(cursor); } + public @NonNull DistributionId getOrCreateDistributionId(@NonNull GroupId.V2 groupId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = GROUP_ID + " = ?"; + String[] args = SqlUtil.buildArgs(groupId); + + try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { DISTRIBUTION_ID }, query, args, null, null, null)) { + if (cursor.moveToFirst()) { + Optional serialized = CursorUtil.getString(cursor, DISTRIBUTION_ID); + + if (serialized.isPresent()) { + return DistributionId.from(serialized.get()); + } else { + Log.w(TAG, "Missing distributionId! Creating one."); + + DistributionId distributionId = DistributionId.create(); + + ContentValues values = new ContentValues(1); + values.put(DISTRIBUTION_ID, distributionId.toString()); + + int count = db.update(TABLE_NAME, values, query, args); + if (count < 1) { + throw new IllegalStateException("Tried to create a distributionId for " + groupId + ", but it doesn't exist!"); + } + + return distributionId; + } + } else { + throw new IllegalStateException("Group " + groupId + " doesn't exist!"); + } + } + } + public GroupId.Mms getOrCreateMmsGroupForMembers(List members) { Collections.sort(members); @@ -436,6 +474,7 @@ public final class GroupDatabase extends Database { if (groupId.isV2()) { contentValues.put(ACTIVE, groupState != null && gv2GroupActive(groupState) ? 1 : 0); + contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString()); } else if (groupId.isV1()) { contentValues.put(ACTIVE, 1); contentValues.put(EXPECTED_V2_ID, groupId.requireV1().deriveV2MigrationGroupId().toString()); @@ -524,6 +563,7 @@ public final class GroupDatabase extends Database { ContentValues contentValues = new ContentValues(); contentValues.put(GROUP_ID, groupIdV2.toString()); contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize()); + contentValues.put(DISTRIBUTION_ID, DistributionId.create().toString()); contentValues.putNull(EXPECTED_V2_ID); List newMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())); @@ -596,6 +636,18 @@ public final class GroupDatabase extends Database { contentValues.put(MEMBERS, RecipientId.toSerializedList(groupMembers)); contentValues.put(ACTIVE, gv2GroupActive(decryptedGroup) ? 1 : 0); + DistributionId distributionId = Objects.requireNonNull(existingGroup.get().getDistributionId()); + + if (existingGroup.isPresent() && existingGroup.get().isV2Group()) { + DecryptedGroupChange change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().getDecryptedGroup(), decryptedGroup); + List removed = DecryptedGroupUtil.removedMembersUuidList(change); + + if (removed.size() > 0) { + Log.i(TAG, removed.size() + " members were removed from group " + groupId + ". Rotating the sender key."); + SenderKeyUtil.rotateOurKey(context, distributionId); + } + } + databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?", new String[]{ groupId.toString() }); @@ -604,7 +656,7 @@ public final class GroupDatabase extends Database { recipientDatabase.setExpireMessages(groupRecipientId, decryptedGroup.getDisappearingMessagesTimer().getDuration()); } - if (groupMembers != null && (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing())) { + if (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing()) { recipientDatabase.setHasGroupsInCommon(groupMembers); } @@ -735,7 +787,7 @@ public final class GroupDatabase extends Database { } - private static List uuidsToRecipientIds(@NonNull List uuids) { + private static @NonNull List uuidsToRecipientIds(@NonNull List uuids) { List groupMembers = new ArrayList<>(uuids.size()); for (UUID uuid : uuids) { @@ -751,11 +803,9 @@ public final class GroupDatabase extends Database { return groupMembers; } - private static List getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { - List uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()); - List recipientIds = uuidsToRecipientIds(uuids); - - return recipientIds; + private static @NonNull List getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { + List uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()); + return uuidsToRecipientIds(uuids); } public @NonNull List getAllGroupV2Ids() { @@ -830,7 +880,8 @@ public final class GroupDatabase extends Database { CursorUtil.requireBoolean(cursor, MMS), CursorUtil.requireBlob(cursor, V2_MASTER_KEY), CursorUtil.requireInt(cursor, V2_REVISION), - CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP)); + CursorUtil.requireBlob(cursor, V2_DECRYPTED_GROUP), + CursorUtil.getString(cursor, DISTRIBUTION_ID).transform(DistributionId::from).orNull()); } @Override @@ -855,6 +906,7 @@ public final class GroupDatabase extends Database { private final boolean active; private final boolean mms; @Nullable private final V2GroupProperties v2GroupProperties; + private final DistributionId distributionId; public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, @@ -870,7 +922,8 @@ public final class GroupDatabase extends Database { boolean mms, @Nullable byte[] groupMasterKeyBytes, int groupRevision, - @Nullable byte[] decryptedGroupBytes) + @Nullable byte[] decryptedGroupBytes, + @Nullable DistributionId distributionId) { this.id = id; this.recipientId = recipientId; @@ -882,6 +935,7 @@ public final class GroupDatabase extends Database { this.relay = relay; this.active = active; this.mms = mms; + this.distributionId = distributionId; V2GroupProperties v2GroupProperties = null; if (groupMasterKeyBytes != null && decryptedGroupBytes != null) { @@ -969,6 +1023,10 @@ public final class GroupDatabase extends Database { return mms; } + public @Nullable DistributionId getDistributionId() { + return distributionId; + } + public boolean isV1Group() { return !mms && !isV2Group(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 4ec34b0b75..0d184c8b0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -9,12 +9,16 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.SqlUtil; import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import javax.annotation.Nullable; + public class GroupReceiptDatabase extends Database { public static final String TABLE_NAME = "group_receipts"; @@ -109,6 +113,23 @@ public class GroupReceiptDatabase extends Database { return results; } + public @Nullable GroupReceiptInfo getGroupReceiptInfo(long mmsId, @NonNull RecipientId recipientId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = MMS_ID + " = ? AND " + RECIPIENT_ID + " = ?"; + String[] args = SqlUtil.buildArgs(mmsId, recipientId); + + try (Cursor cursor = db.query(TABLE_NAME, null, query, args, null, null, "1")) { + if (cursor.moveToFirst()) { + return new GroupReceiptInfo(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))), + cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)), + cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)), + cursor.getInt(cursor.getColumnIndexOrThrow(UNIDENTIFIED)) == 1); + } + } + + return null; + } + void deleteRowsForMessage(long mmsId) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)}); 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 411ba24380..77c44e2b4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -158,7 +158,8 @@ 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 @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp); + public abstract void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId); 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 cb77773cd7..41841205e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; -import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; @@ -1443,7 +1442,12 @@ public class MmsDatabase extends MessageDatabase { } @Override - public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + public @NonNull InsertResult insertChatSessionRefreshedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + throw new UnsupportedOperationException(); + } + + @Override + public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) { throw new UnsupportedOperationException(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 48658050a2..866d355038 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -74,6 +74,7 @@ public interface MmsSmsColumns { protected static final long INCOMING_VIDEO_CALL_TYPE = 10; protected static final long OUTGOING_VIDEO_CALL_TYPE = 11; protected static final long GROUP_CALL_TYPE = 12; + protected static final long BAD_DECRYPT_TYPE = 13; protected static final long BASE_INBOX_TYPE = 20; protected static final long BASE_OUTBOX_TYPE = 21; @@ -196,6 +197,10 @@ public interface MmsSmsColumns { return (type & BASE_TYPE_MASK) == INVALID_MESSAGE_TYPE; } + public static boolean isBadDecryptType(long type) { + return (type & BASE_TYPE_MASK) == BAD_DECRYPT_TYPE; + } + public static boolean isSecureType(long type) { return (type & SECURE_MESSAGE_BIT) != 0; } @@ -298,7 +303,7 @@ public interface MmsSmsColumns { return (type & GROUP_QUIT_BIT) != 0; } - public static boolean isFailedDecryptType(long type) { + public static boolean isChatSessionRefresh(long type) { return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptDatabase.java new file mode 100644 index 0000000000..1630e8cca7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptDatabase.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.database; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; + +/** + * Holds information about messages we've sent out retry receipts for. + */ +public final class PendingRetryReceiptDatabase extends Database { + + public static final String TABLE_NAME = "pending_retry_receipts"; + + private static final String ID = "_id"; + private static final String AUTHOR = "author"; + private static final String DEVICE = "device"; + private static final String SENT_TIMESTAMP = "sent_timestamp"; + private static final String RECEIVED_TIMESTAMP = "received_timestamp"; + private static final String THREAD_ID = "thread_id"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + AUTHOR + " TEXT NOT NULL, " + + DEVICE + " INTEGER NOT NULL, " + + SENT_TIMESTAMP + " INTEGER NOT NULL, " + + RECEIVED_TIMESTAMP + " TEXT NOT NULL, " + + THREAD_ID + " INTEGER NOT NULL, " + + "UNIQUE(" + AUTHOR + "," + SENT_TIMESTAMP + ") ON CONFLICT REPLACE);"; + + PendingRetryReceiptDatabase(Context context, SQLCipherOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public void insert(@NonNull RecipientId author, int authorDevice, long sentTimestamp, long receivedTimestamp, long threadId) { + ContentValues values = new ContentValues(); + values.put(AUTHOR, author.serialize()); + values.put(DEVICE, authorDevice); + values.put(SENT_TIMESTAMP, sentTimestamp); + values.put(RECEIVED_TIMESTAMP, receivedTimestamp); + values.put(THREAD_ID, threadId); + + databaseHelper.getWritableDatabase().insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + + public @Nullable PendingRetryReceiptModel get(@NonNull RecipientId author, long sentTimestamp) { + String query = AUTHOR + " = ? AND " + SENT_TIMESTAMP + " = ?"; + String[] args = SqlUtil.buildArgs(author, sentTimestamp); + + try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null)) { + if (cursor.moveToFirst()) { + return fromCursor(cursor); + } + } + + return null; + } + + public @Nullable PendingRetryReceiptModel getOldest() { + try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, RECEIVED_TIMESTAMP + " ASC", "1")) { + if (cursor.moveToFirst()) { + return fromCursor(cursor); + } + } + + return null; + } + + public void delete(long id) { + databaseHelper.getWritableDatabase().delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(id)); + } + + + private static @NonNull PendingRetryReceiptModel fromCursor(@NonNull Cursor cursor) { + return new PendingRetryReceiptModel(CursorUtil.requireLong(cursor, ID), + RecipientId.from(CursorUtil.requireString(cursor, AUTHOR)), + CursorUtil.requireInt(cursor, DEVICE), + CursorUtil.requireLong(cursor, SENT_TIMESTAMP), + CursorUtil.requireLong(cursor, RECEIVED_TIMESTAMP), + CursorUtil.requireLong(cursor, THREAD_ID)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index d57b8bc1ba..f2dba7b65a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -156,11 +156,16 @@ public class RecipientDatabase extends Database { private static final String IDENTITY_STATUS = "identity_status"; private static final String IDENTITY_KEY = "identity_key"; + /** + * Values that represent the index in the capabilities bitmask. Each index can store a 2-bit + * value, which in this case is the value of {@link Recipient.Capability}. + */ private static final class Capabilities { static final int BIT_LENGTH = 2; static final int GROUPS_V2 = 0; static final int GROUPS_V1_MIGRATION = 1; + static final int SENDER_KEY = 2; } private static final String[] RECIPIENT_PROJECTION = new String[] { @@ -1632,6 +1637,7 @@ public class RecipientDatabase extends Database { value = Bitmask.update(value, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv2()).serialize()); value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration()).serialize()); + value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey()).serialize()); ContentValues values = new ContentValues(1); values.put(CAPABILITIES, value); @@ -3137,6 +3143,7 @@ public class RecipientDatabase extends Database { private final long capabilities; private final Recipient.Capability groupsV2Capability; private final Recipient.Capability groupsV1MigrationCapability; + private final Recipient.Capability senderKeyCapability; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -3145,9 +3152,9 @@ public class RecipientDatabase extends Database { private final AvatarColor avatarColor; private final String about; private final String aboutEmoji; - private final SyncExtras syncExtras; - private final Recipient.Extras extras; - private final boolean hasGroupsInCommon; + private final SyncExtras syncExtras; + private final Recipient.Extras extras; + private final boolean hasGroupsInCommon; RecipientSettings(@NonNull RecipientId id, @Nullable UUID uuid, @@ -3227,6 +3234,7 @@ public class RecipientDatabase extends Database { this.capabilities = capabilities; this.groupsV2Capability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V2, Capabilities.BIT_LENGTH)); this.groupsV1MigrationCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH)); + this.senderKeyCapability = Recipient.Capability.deserialize((int) Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH)); this.insightsBannerTier = insightsBannerTier; this.storageId = storageId; this.mentionSetting = mentionSetting; @@ -3376,6 +3384,10 @@ public class RecipientDatabase extends Database { return groupsV1MigrationCapability; } + public @NonNull Recipient.Capability getSenderKeyCapability() { + return senderKeyCapability; + } + public @Nullable byte[] getStorageId() { return storageId; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeyDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeyDatabase.java new file mode 100644 index 0000000000..839f76481d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeyDatabase.java @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.database; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; + +import java.io.IOException; + +/** + * Stores all of the sender keys -- both the ones we create, and the ones we're told about. + * + * When working with SenderKeys, keep this in mind: they're not *really* keys. They're sessions. + * The name is largely historical, and there's too much momentum to change it. + */ +public class SenderKeyDatabase extends Database { + + private static final String TAG = Log.tag(SenderKeyDatabase.class); + + public static final String TABLE_NAME = "sender_keys"; + + private static final String ID = "_id"; + public static final String RECIPIENT_ID = "recipient_id"; + public static final String DEVICE = "device"; + public static final String DISTRIBUTION_ID = "distribution_id"; + public static final String RECORD = "record"; + public static final String CREATED_AT = "created_at"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + RECIPIENT_ID + " INTEGER NOT NULL, " + + DEVICE + " INTEGER NOT NULL, " + + DISTRIBUTION_ID + " TEXT NOT NULL, " + + RECORD + " BLOB NOT NULL, " + + CREATED_AT + " INTEGER NOT NULL, " + + "UNIQUE(" + RECIPIENT_ID + "," + DEVICE + ", " + DISTRIBUTION_ID + ") ON CONFLICT REPLACE);"; + + SenderKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public void store(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId, @NonNull SenderKeyRecord record) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(DEVICE, deviceId); + values.put(DISTRIBUTION_ID, distributionId.toString()); + values.put(RECORD, record.serialize()); + values.put(CREATED_AT, System.currentTimeMillis()); + + db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + + public @Nullable SenderKeyRecord load(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?"; + String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId); + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{ RECORD }, query, args, null, null, null)) { + if (cursor.moveToFirst()) { + try { + return new SenderKeyRecord(CursorUtil.requireBlob(cursor, RECORD)); + } catch (IOException e) { + Log.w(TAG, e); + } + } + } + + return null; + } + + /** + * Gets when the sender key session was created, or -1 if it doesn't exist. + */ + public long getCreatedTime(@NonNull RecipientId recipientId, int deviceId, @NonNull DistributionId distributionId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ? AND " + DISTRIBUTION_ID + " = ?"; + String[] args = SqlUtil.buildArgs(recipientId, deviceId, distributionId); + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{ CREATED_AT }, query, args, null, null, null)) { + if (cursor.moveToFirst()) { + return CursorUtil.requireLong(cursor, CREATED_AT); + } + } + + return -1; + } + + /** + * Removes all sender key session state for all devices for the provided recipient-distributionId pair. + */ + public void deleteAllFor(@NonNull RecipientId recipientId, @NonNull DistributionId distributionId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String query = RECIPIENT_ID + " = ? AND " + DISTRIBUTION_ID + " = ?"; + String[] args = SqlUtil.buildArgs(recipientId, distributionId); + + db.delete(TABLE_NAME, query, args); + } + + /** + * Deletes all database state. + */ + public void deleteAll() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete(TABLE_NAME, null, null); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeySharedDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeySharedDatabase.java new file mode 100644 index 0000000000..a9bdfb823d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SenderKeySharedDatabase.java @@ -0,0 +1,141 @@ +package org.thoughtcrime.securesms.database; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * Keeps track of which recipients are aware of which distributionIds. For the storage of sender + * keys themselves, see {@link SenderKeyDatabase}. + */ +public class SenderKeySharedDatabase extends Database { + + private static final String TAG = Log.tag(SenderKeySharedDatabase.class); + + public static final String TABLE_NAME = "sender_key_shared"; + + private static final String ID = "_id"; + public static final String DISTRIBUTION_ID = "distribution_id"; + public static final String ADDRESS = "address"; + public static final String DEVICE = "device"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + DISTRIBUTION_ID + " TEXT NOT NULL, " + + ADDRESS + " TEXT NOT NULL, " + + DEVICE + " INTEGER NOT NULL, " + + "UNIQUE(" + DISTRIBUTION_ID + "," + ADDRESS + ", " + DEVICE + ") ON CONFLICT REPLACE);"; + + SenderKeySharedDatabase(Context context, SQLCipherOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + /** + * Mark that a distributionId has been shared with the provided recipients + */ + public void markAsShared(@NonNull DistributionId distributionId, @NonNull Collection addresses) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + for (SignalProtocolAddress address : addresses) { + ContentValues values = new ContentValues(); + values.put(ADDRESS, address.getName()); + values.put(DEVICE, address.getDeviceId()); + values.put(DISTRIBUTION_ID, distributionId.toString()); + + db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Get the set of recipientIds that know about the distributionId in question. + */ + public @NonNull Set getSharedWith(@NonNull DistributionId distributionId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = DISTRIBUTION_ID + " = ?"; + String[] args = SqlUtil.buildArgs(distributionId); + + Set addresses = new HashSet<>(); + + try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ADDRESS, DEVICE }, query, args, null, null, null)) { + while (cursor.moveToNext()) { + String address = CursorUtil.requireString(cursor, ADDRESS); + int device = CursorUtil.requireInt(cursor, DEVICE); + + addresses.add(new SignalProtocolAddress(address, device)); + } + } + + return addresses; + } + + /** + * Clear the shared statuses for all provided addresses. + */ + public void delete(@NonNull DistributionId distributionId, @NonNull Collection addresses) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String query = DISTRIBUTION_ID + " = ? AND " + ADDRESS + " = ? AND " + DEVICE + " = ?"; + + db.beginTransaction(); + try { + for (SignalProtocolAddress address : addresses) { + db.delete(TABLE_NAME, query, SqlUtil.buildArgs(distributionId, address.getName(), address.getDeviceId())); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + /** + * Clear all shared statuses for a given distributionId. + */ + public void deleteAllFor(@NonNull DistributionId distributionId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete(TABLE_NAME, DISTRIBUTION_ID + " = ?", SqlUtil.buildArgs(distributionId)); + } + + /** + * Clear all shared statuses for a given recipientId. + */ + public void deleteAllFor(@NonNull RecipientId recipientId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + Recipient recipient = Recipient.resolved(recipientId); + + if (recipient.hasUuid()) { + db.delete(TABLE_NAME, ADDRESS + " = ?", SqlUtil.buildArgs(recipient.getUuid().get().toString())); + } else { + Log.w(TAG, "Recipient doesn't have a UUID! " + recipientId); + } + } + + /** + * Clears all database content. + */ + public void deleteAll() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete(TABLE_NAME, null, null); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java index 7a811a1b5c..81106cee72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.java @@ -11,13 +11,13 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.SqlUtil; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -72,6 +72,36 @@ public class SessionDatabase extends Database { return null; } + public @NonNull List load(@NonNull List ids) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + List sessions = new ArrayList<>(ids.size()); + + database.beginTransaction(); + try { + String[] projection = new String[]{RECORD}; + String query = RECIPIENT_ID + " = ? AND " + DEVICE + " = ?"; + + for (RecipientDevice id : ids) { + String[] args = SqlUtil.buildArgs(id.getRecipientId(), id.getDevice()); + + try (Cursor cursor = database.query(TABLE_NAME, projection, query, args, null, null, null)) { + if (cursor.moveToFirst()) { + try { + sessions.add(new SessionRecord(cursor.getBlob(cursor.getColumnIndexOrThrow(RECORD)))); + } catch (IOException e) { + Log.w(TAG, e); + } + } + } + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + + return sessions; + } + public @NonNull List getAllFor(@NonNull RecipientId recipientId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); List results = new LinkedList<>(); @@ -180,4 +210,22 @@ public class SessionDatabase extends Database { return record; } } + + public static final class RecipientDevice { + private final RecipientId recipientId; + private final int device; + + public RecipientDevice(@NonNull RecipientId recipientId, int device) { + this.recipientId = recipientId; + this.device = device; + } + + public @NonNull RecipientId getRecipientId() { + return recipientId; + } + + public int getDevice() { + return device; + } + } } 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 db027d1d4c..5ae3603e8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -1156,7 +1156,7 @@ public class SmsDatabase extends MessageDatabase { } @Override - public @NonNull InsertResult insertDecryptionFailedMessage(@NonNull RecipientId recipientId, long senderDeviceId, long sentTimestamp) { + public @NonNull InsertResult insertChatSessionRefreshedMessage(@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; @@ -1185,6 +1185,28 @@ public class SmsDatabase extends MessageDatabase { return new InsertResult(messageId, threadId); } + @Override + public void insertBadDecryptMessage(@NonNull RecipientId recipientId, int senderDevice, long sentTimestamp, long receivedTimestamp, long threadId) { + ContentValues values = new ContentValues(); + values.put(RECIPIENT_ID, recipientId.serialize()); + values.put(ADDRESS_DEVICE_ID, senderDevice); + values.put(DATE_SENT, sentTimestamp); + values.put(DATE_RECEIVED, receivedTimestamp); + values.put(DATE_SERVER, -1); + values.put(READ, 0); + values.put(TYPE, Types.BAD_DECRYPT_TYPE); + values.put(THREAD_ID, threadId); + + databaseHelper.getWritableDatabase().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)); + } + @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/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index c7cf687241..be75935876 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -40,10 +40,13 @@ import org.thoughtcrime.securesms.database.MentionDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PaymentDatabase; +import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RemappedRecordsDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; +import org.thoughtcrime.securesms.database.SenderKeyDatabase; +import org.thoughtcrime.securesms.database.SenderKeySharedDatabase; import org.thoughtcrime.securesms.database.SessionDatabase; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; @@ -73,6 +76,7 @@ import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Triple; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.signalservice.api.push.DistributionId; import java.io.ByteArrayInputStream; import java.io.File; @@ -192,8 +196,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int CHAT_COLORS = 100; private static final int AVATAR_COLORS = 101; private static final int EMOJI_SEARCH = 102; + private static final int SENDER_KEY = 103; - private static final int DATABASE_VERSION = 102; + private static final int DATABASE_VERSION = 103; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -221,6 +226,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE); db.execSQL(SignedPreKeyDatabase.CREATE_TABLE); db.execSQL(SessionDatabase.CREATE_TABLE); + db.execSQL(SenderKeyDatabase.CREATE_TABLE); + db.execSQL(SenderKeySharedDatabase.CREATE_TABLE); + db.execSQL(PendingRetryReceiptDatabase.CREATE_TABLE); db.execSQL(StickerDatabase.CREATE_TABLE); db.execSQL(UnknownStorageIdDatabase.CREATE_TABLE); db.execSQL(MentionDatabase.CREATE_TABLE); @@ -1513,6 +1521,44 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL("CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED)"); } + if (oldVersion < SENDER_KEY && !SqlUtil.tableExists(db, "sender_keys")) { + db.execSQL("CREATE TABLE sender_keys (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "recipient_id INTEGER NOT NULL, " + + "device INTEGER NOT NULL, " + + "distribution_id TEXT NOT NULL, " + + "record BLOB NOT NULL, " + + "created_at INTEGER NOT NULL, " + + "UNIQUE(recipient_id, device, distribution_id) ON CONFLICT REPLACE)"); + + db.execSQL("CREATE TABLE sender_key_shared (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "distribution_id TEXT NOT NULL, " + + "address TEXT NOT NULL, " + + "device INTEGER NOT NULL, " + + "UNIQUE(distribution_id, address, device) ON CONFLICT REPLACE)"); + + db.execSQL("CREATE TABLE pending_retry_receipts (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "author TEXT NOT NULL, " + + "device INTEGER NOT NULL, " + + "sent_timestamp INTEGER NOT NULL, " + + "received_timestamp TEXT NOT NULL, " + + "thread_id INTEGER NOT NULL, " + + "UNIQUE(author, sent_timestamp) ON CONFLICT REPLACE);"); + + db.execSQL("ALTER TABLE groups ADD COLUMN distribution_id TEXT DEFAULT NULL"); + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_distribution_id_index ON groups (distribution_id)"); + + try (Cursor cursor = db.query("groups", new String[] { "group_id" }, "LENGTH(group_id) = 85", null, null, null, null)) { + while (cursor.moveToNext()) { + String groupId = cursor.getString(cursor.getColumnIndexOrThrow("group_id")); + ContentValues values = new ContentValues(); + + values.put("distribution_id", DistributionId.create().toString()); + + db.update("groups", values, "group_id = ?", new String[] { groupId }); + } + } + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 018ccc2983..fa7bba8d7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -103,7 +103,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { - if (MmsDatabase.Types.isFailedDecryptType(type)) { + if (MmsDatabase.Types.isChatSessionRefresh(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message)); } else if (MmsDatabase.Types.isDuplicateMessageType(type)) { return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); 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 0aaf43400f..b4915b0aa3 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 @@ -57,7 +57,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -191,8 +190,10 @@ 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()) { + } else if (isChatSessionRefresh()) { return staticUpdateDescription(context.getString(R.string.MessageRecord_chat_session_refreshed), R.drawable.ic_refresh_16); + } else if (isBadDecryptType()) { + return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_a_message_from_s_couldnt_be_delivered, r.getDisplayName(context)), R.drawable.ic_error_outline_14); } return null; @@ -458,6 +459,10 @@ public abstract class MessageRecord extends DisplayRecord { return SmsDatabase.Types.isCorruptedKeyExchange(type); } + public boolean isBadDecryptType() { + return MmsSmsColumns.Types.isBadDecryptType(type); + } + public boolean isInvalidVersionKeyExchange() { return SmsDatabase.Types.isInvalidVersionKeyExchange(type); } @@ -476,8 +481,8 @@ public abstract class MessageRecord extends DisplayRecord { public boolean isUpdate() { return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() || - isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || - isProfileChange() || isGroupV1MigrationEvent() || isFailedDecryptionType(); + isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() || + isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType(); } public boolean isMediaPending() { @@ -512,8 +517,8 @@ public abstract class MessageRecord extends DisplayRecord { return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure()); } - public boolean isFailedDecryptionType() { - return MmsSmsColumns.Types.isFailedDecryptType(type); + public boolean isChatSessionRefresh() { + return MmsSmsColumns.Types.isChatSessionRefresh(type); } public boolean isInMemoryMessageRecord() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/PendingRetryReceiptModel.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/PendingRetryReceiptModel.kt new file mode 100644 index 0000000000..9f9566e49f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/PendingRetryReceiptModel.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.database.model + +import org.thoughtcrime.securesms.recipients.RecipientId + +/** A model for [org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase] */ +data class PendingRetryReceiptModel( + val id: Long, + val author: RecipientId, + val authorDevice: Int, + val sentTimestamp: Long, + val receivedTimestamp: Long, + val threadId: Long +) 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 be02a80bc3..8bd6956274 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 @@ -65,7 +65,7 @@ public class SmsMessageRecord extends MessageRecord { @Override public SpannableString getDisplayBody(@NonNull Context context) { - if (SmsDatabase.Types.isFailedDecryptType(type)) { + if (SmsDatabase.Types.isChatSessionRefresh(type)) { 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)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 63574d3df3..2023975bb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.database.DatabaseObserver; +import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; import org.thoughtcrime.securesms.groups.GroupsV2Authorization; import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache; import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; @@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager; +import org.thoughtcrime.securesms.service.PendingRetryReceiptManager; import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; import org.thoughtcrime.securesms.service.webrtc.SignalCallManager; import org.thoughtcrime.securesms.shakereport.ShakeToReport; @@ -87,6 +89,7 @@ public class ApplicationDependencies { private static volatile SignalCallManager signalCallManager; private static volatile ShakeToReport shakeToReport; private static volatile OkHttpClient okHttpClient; + private static volatile PendingRetryReceiptManager pendingRetryReceiptManager; @MainThread public static void init(@NonNull Application application, @NonNull Provider provider) { @@ -362,6 +365,18 @@ public class ApplicationDependencies { return viewOnceMessageManager; } + public static @NonNull PendingRetryReceiptManager getPendingRetryReceiptManager() { + if (pendingRetryReceiptManager == null) { + synchronized (LOCK) { + if (pendingRetryReceiptManager == null) { + pendingRetryReceiptManager = provider.providePendingRetryReceiptManager(); + } + } + } + + return pendingRetryReceiptManager; + } + public static @NonNull ExpiringMessageManager getExpiringMessageManager() { if (expiringMessageManager == null) { synchronized (LOCK) { @@ -492,5 +507,6 @@ public class ApplicationDependencies { @NonNull ShakeToReport provideShakeToReport(); @NonNull AppForegroundObserver provideAppForegroundObserver(); @NonNull SignalCallManager provideSignalCallManager(); + @NonNull PendingRetryReceiptManager providePendingRetryReceiptManager(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index d29fec5f79..aabc6b78f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; import org.thoughtcrime.securesms.service.ExpiringMessageManager; +import org.thoughtcrime.securesms.service.PendingRetryReceiptManager; import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; import org.thoughtcrime.securesms.service.webrtc.SignalCallManager; import org.thoughtcrime.securesms.shakereport.ShakeToReport; @@ -251,6 +252,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return new SignalCallManager(context); } + @Override + public @NonNull PendingRetryReceiptManager providePendingRetryReceiptManager() { + return new PendingRetryReceiptManager(context); + } + private static class DynamicCredentialsProvider implements CredentialsProvider { private final Context context; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java index 1bb4eb94a5..f91d8c4702 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AutomaticSessionResetJob.java @@ -7,7 +7,6 @@ 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.database.RecipientDatabase; import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; @@ -16,7 +15,6 @@ 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.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; @@ -87,6 +85,7 @@ public class AutomaticSessionResetJob extends BaseJob { @Override protected void onRun() throws Exception { SessionUtil.archiveSession(context, recipientId, deviceId); + DatabaseFactory.getSenderKeySharedDatabase(context).deleteAllFor(recipientId); insertLocalMessage(); if (FeatureFlags.automaticSessionReset()) { @@ -122,7 +121,7 @@ public class AutomaticSessionResetJob extends BaseJob { } private void insertLocalMessage() { - MessageDatabase.InsertResult result = DatabaseFactory.getSmsDatabase(context).insertDecryptionFailedMessage(recipientId, deviceId, sentTimestamp); + MessageDatabase.InsertResult result = DatabaseFactory.getSmsDatabase(context).insertChatSessionRefreshedMessage(recipientId, deviceId, sentTimestamp); ApplicationDependencies.getMessageNotifier().updateNotification(context, result.getThreadId()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java index 5106f3c091..3602d14a10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java @@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -161,7 +162,7 @@ public class GroupCallUpdateSendJob extends BaseJob { GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush()); } - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); return GroupSendJobHelper.getCompletedSends(context, results); } 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 bbc3f61572..ca78e9da13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -137,8 +137,10 @@ public final class JobManagerFactories { put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory()); put(RotateProfileKeyJob.KEY, new RotateProfileKeyJob.Factory()); put(RotateSignedPreKeyJob.KEY, new RotateSignedPreKeyJob.Factory()); + put(SenderKeyDistributionSendJob.KEY, new SenderKeyDistributionSendJob.Factory()); put(SendDeliveryReceiptJob.KEY, new SendDeliveryReceiptJob.Factory()); put(SendReadReceiptJob.KEY, new SendReadReceiptJob.Factory(application)); + put(SendRetryReceiptJob.KEY, new SendRetryReceiptJob.Factory()); put(SendViewedReceiptJob.KEY, new SendViewedReceiptJob.Factory(application)); put(ServiceOutageDetectionJob.KEY, new ServiceOutageDetectionJob.Factory()); put(SmsReceiveJob.KEY, new SmsReceiveJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java index bc5e50a960..4aa3568151 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -141,7 +142,7 @@ public class LeaveGroupJob extends BaseJob { .asGroupMessage(serviceGroup); - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java index af310d8440..b8b03b17cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceBlockedUpdateJob.java @@ -88,8 +88,8 @@ public class MultiDeviceBlockedUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(blockedIndividuals, blockedGroups)), + UnidentifiedAccessUtil.getAccessForSync(context)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java index b9f3cd0f11..16efb4e816 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceConfigurationUpdateJob.java @@ -95,10 +95,10 @@ public class MultiDeviceConfigurationUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled), - Optional.of(unidentifiedDeliveryIndicatorsEnabled), - Optional.of(typingIndicatorsEnabled), - Optional.of(linkPreviewsEnabled))), + messageSender.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(new ConfigurationMessage(Optional.of(readReceiptsEnabled), + Optional.of(unidentifiedDeliveryIndicatorsEnabled), + Optional.of(typingIndicatorsEnabled), + Optional.of(linkPreviewsEnabled))), UnidentifiedAccessUtil.getAccessForSync(context)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index b253400593..cd31b257cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -271,8 +271,8 @@ public class MultiDeviceContactUpdateJob extends BaseJob { .withLength(length) .withResumableUploadSpec(messageSender.getResumableUploadSpec()); - messageSender.sendMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream.build(), complete)), + UnidentifiedAccessUtil.getAccessForSync(context)); } catch (IOException ioe) { throw new NetworkException(ioe); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java index c3b1e47ff3..bbae7085aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceGroupUpdateJob.java @@ -33,7 +33,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -174,8 +173,8 @@ public class MultiDeviceGroupUpdateJob extends BaseJob { attachmentStream = SignalServiceAttachment.emptyStream("application/octet-stream"); } - messageSender.sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream), + UnidentifiedAccessUtil.getAccessForSync(context)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java index 48846640d0..51520908fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java @@ -68,8 +68,8 @@ public class MultiDeviceKeysUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); - messageSender.sendMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java index 10fa3c3032..539818054b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java @@ -114,8 +114,8 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob { response = MessageRequestResponseMessage.forIndividual(RecipientUtil.toSignalServiceAddress(context, recipient), localToRemoteType(type)); } - messageSender.sendMessage(SignalServiceSyncMessage.forMessageRequestResponse(response), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forMessageRequestResponse(response), + UnidentifiedAccessUtil.getAccessForSync(context)); } private static MessageRequestResponseMessage.Type localToRemoteType(@NonNull Type type) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java index 5e6230102f..f943768235 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceOutgoingPaymentSyncJob.java @@ -114,8 +114,8 @@ public final class MultiDeviceOutgoingPaymentSyncJob extends BaseJob { ApplicationDependencies.getSignalServiceMessageSender() - .sendMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage), - UnidentifiedAccessUtil.getAccessForSync(context)); + .sendSyncMessage(SignalServiceSyncMessage.forOutgoingPayment(outgoingPaymentMessage), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java index 1c6bb17672..5141a86096 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileContentUpdateJob.java @@ -58,8 +58,8 @@ public class MultiDeviceProfileContentUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java index ab0ab3e1f1..04dd2595d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceProfileKeyUpdateJob.java @@ -98,7 +98,7 @@ public class MultiDeviceProfileKeyUpdateJob extends BaseJob { SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, false)); - messageSender.sendMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java index a0ccccf5d9..f7eaf78b72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceReadUpdateJob.java @@ -120,7 +120,7 @@ public class MultiDeviceReadUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forRead(readMessages), UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java index 9eb921b364..9a04969a3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackOperationJob.java @@ -92,8 +92,8 @@ public class MultiDeviceStickerPackOperationJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); StickerPackOperationMessage stickerPackOperation = new StickerPackOperationMessage(packIdBytes, packKeyBytes, remoteType); - messageSender.sendMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(Collections.singletonList(stickerPackOperation)), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java index fba75e4b25..fd8571b10a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStickerPackSyncJob.java @@ -80,8 +80,8 @@ public class MultiDeviceStickerPackSyncJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forStickerPackOperations(operations), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forStickerPackOperations(operations), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java index 5fccb32c81..e236362c19 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceStorageSyncRequestJob.java @@ -58,8 +58,8 @@ public class MultiDeviceStorageSyncRequestJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST), - UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.STORAGE_MANIFEST), + UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java index 0b6266a2b9..6b3eede0ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceVerifiedUpdateJob.java @@ -109,8 +109,8 @@ public class MultiDeviceVerifiedUpdateJob extends BaseJob { SignalServiceAddress verifiedAddress = RecipientUtil.toSignalServiceAddress(context, recipient); VerifiedMessage verifiedMessage = new VerifiedMessage(verifiedAddress, new IdentityKey(identityKey, 0), verifiedState, timestamp); - messageSender.sendMessage(SignalServiceSyncMessage.forVerified(verifiedMessage), - UnidentifiedAccessUtil.getAccessFor(context, recipient)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage), + UnidentifiedAccessUtil.getAccessFor(context, recipient)); } catch (InvalidKeyException e) { throw new IOException(e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java index 13794158e5..269ded31c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewOnceOpenJob.java @@ -85,7 +85,7 @@ public class MultiDeviceViewOnceOpenJob extends BaseJob { Recipient recipient = Recipient.resolved(RecipientId.from(messageId.recipientId)); ViewOnceOpenMessage openMessage = new ViewOnceOpenMessage(RecipientUtil.toSignalServiceAddress(context, recipient), messageId.timestamp); - messageSender.sendMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewOnceOpen(openMessage), UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java index d232cc98b7..bca65d88be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceViewedUpdateJob.java @@ -120,7 +120,7 @@ public class MultiDeviceViewedUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - messageSender.sendMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context)); + messageSender.sendSyncMessage(SignalServiceSyncMessage.forViewed(viewedMessages), UnidentifiedAccessUtil.getAccessForSync(context)); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java index 582ef305b6..7c72008b2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PaymentNotificationSendJob.java @@ -16,6 +16,7 @@ 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.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -83,7 +84,7 @@ public final class PaymentNotificationSendJob extends BaseJob { PaymentDatabase paymentDatabase = DatabaseFactory.getPaymentDatabase(context); Recipient recipient = Recipient.resolved(recipientId); SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - SignalServiceAddress addresses = RecipientUtil.toSignalServiceAddress(context, recipient); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); Optional unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); PaymentDatabase.PaymentTransaction payment = paymentDatabase.getPayment(uuid); @@ -102,7 +103,7 @@ public final class PaymentNotificationSendJob extends BaseJob { .withPayment(new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()))) .build(); - SendMessageResult sendMessageResult = messageSender.sendMessage(addresses, unidentifiedAccess, dataMessage); + SendMessageResult sendMessageResult = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.DEFAULT, dataMessage); if (sendMessageResult.getIdentityFailure() != null) { Log.w(TAG, "Identity failure for " + recipient.getId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java index 2fa4dc0826..1159119be7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -22,6 +22,7 @@ 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.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -159,7 +160,7 @@ public class ProfileKeySendJob extends BaseJob { dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId())); } - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.IMPLICIT, dataMessage.build()); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 93da38e547..2566ee16e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobLogger; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; @@ -43,6 +44,7 @@ import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -291,6 +293,7 @@ public final class PushGroupSendJob extends PushSendJob { try { rotateSenderCertificateIfNecessary(); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); GroupId.Push groupId = groupRecipient.requireGroupId().requirePush(); Optional profileKey = getProfileKey(groupRecipient); @@ -327,7 +330,7 @@ public final class PushGroupSendJob extends PushSendJob { .withExpiration(groupRecipient.getExpireMessages()) .asGroupMessage(group) .build(); - return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage); + return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage); } else { MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties(); @@ -345,7 +348,7 @@ public final class PushGroupSendJob extends PushSendJob { .build(); Log.i(TAG, JobLogger.format(this, "Beginning update send.")); - return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage); + return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage); } } else { SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder() @@ -367,7 +370,12 @@ public final class PushGroupSendJob extends PushSendJob { .build(); Log.i(TAG, JobLogger.format(this, "Beginning message send.")); - return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupMessage); + + if (groupRecipient.isPushV2Group()) { + return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage); + } else { + return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage); + } } } catch (ServerRejectedException e) { throw new UndeliverableMessageException(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java index 382c1d3029..6a3b82b957 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java @@ -11,29 +11,25 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.local.DecryptedGroup; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; import org.thoughtcrime.securesms.net.NotPushRegisteredException; 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.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.thoughtcrime.securesms.util.GroupUtil; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -133,8 +129,9 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob { throw new NotPushRegisteredException(); } - List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); - List completions = deliver(destinations); + GroupId.V2 groupId = GroupId.v2(GroupUtil.requireMasterKey(groupContextV2.getMasterKey().toByteArray())); + List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); + List completions = deliver(destinations, groupId); for (Recipient completion : completions) { recipients.remove(completion.getId()); @@ -161,20 +158,16 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob { Log.w(TAG, "Failed to send remote delete to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") ); } - private @NonNull List deliver(@NonNull List destinations) + private @NonNull List deliver(@NonNull List destinations, @NonNull GroupId.V2 groupId) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);; - SignalServiceGroupV2 group = SignalServiceGroupV2.fromProtobuf(groupContextV2); SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() .withTimestamp(timestamp) .asGroupMessage(group) .build(); - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, groupDataMessage); + List results = GroupSendUtil.sendDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java index 3f05dfe0a8..f8c5e7c94b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; @@ -124,9 +125,10 @@ public class PushGroupUpdateJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); Recipient recipient = Recipient.resolved(source); - messageSender.sendMessage(RecipientUtil.toSignalServiceAddress(context, recipient), - UnidentifiedAccessUtil.getAccessFor(context, recipient), - message); + messageSender.sendDataMessage(RecipientUtil.toSignalServiceAddress(context, recipient), + UnidentifiedAccessUtil.getAccessFor(context, recipient), + ContentHint.DEFAULT, + message); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index b7c64172d6..8093638a16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -221,10 +222,10 @@ public class PushMediaSendJob extends PushSendJob { Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess); - messageSender.sendMessage(syncMessage, syncAccess); + messageSender.sendSyncMessage(syncMessage, syncAccess); return syncAccess.isPresent(); } else { - return messageSender.sendMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), mediaMessage).getSuccess().isUnidentified(); + return messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage).getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { warn(TAG, String.valueOf(message.getSentTimeMillis()), e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index d17f6d5046..d727117b43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -173,10 +174,10 @@ public class PushTextSendJob extends PushSendJob { Optional syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess); - messageSender.sendMessage(syncMessage, syncAccess); + messageSender.sendSyncMessage(syncMessage, syncAccess); return syncAccess.isPresent(); } else { - return messageSender.sendMessage(address, unidentifiedAccess, textSecureMessage).getSuccess().isUnidentified(); + return messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage).getSuccess().isUnidentified(); } } catch (UnregisteredUserException e) { warn(TAG, "Failure", e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java index 53a4bdcd76..11e318b10d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.net.NotPushRegisteredException; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -25,10 +26,12 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; @@ -216,18 +219,25 @@ public class ReactionSendJob extends BaseJob { private @NonNull List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations, @NonNull Recipient targetAuthor, long targetSentTimestamp) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);; - SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(System.currentTimeMillis()) - .withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp)); + SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(System.currentTimeMillis()) + .withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp)); if (conversationRecipient.isGroup()) { GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush()); } - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + List results; + + if (conversationRecipient.isPushV2Group()) { + results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build()); + } else { + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); + List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);; + + results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); + } return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java index 63a4b0e21f..dfbd7d918f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.net.NotPushRegisteredException; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; @@ -24,6 +25,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -172,18 +174,25 @@ public class RemoteDeleteSendJob extends BaseJob { private @NonNull List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations, long targetSentTimestamp) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); - SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(System.currentTimeMillis()) - .withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp)); + SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(System.currentTimeMillis()) + .withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp)); if (conversationRecipient.isGroup()) { GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush()); } - List results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build()); + List results; + + if (conversationRecipient.isPushV2Group()) { + results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build()); + } else { + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); + List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); + + results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); + } return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java index 080e433739..8e1c6a2bd8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; @@ -85,9 +86,10 @@ public class RequestGroupInfoJob extends BaseJob { SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); Recipient recipient = Recipient.resolved(source); - messageSender.sendMessage(RecipientUtil.toSignalServiceAddress(context, recipient), - UnidentifiedAccessUtil.getAccessFor(context, recipient), - message); + messageSender.sendDataMessage(RecipientUtil.toSignalServiceAddress(context, recipient), + UnidentifiedAccessUtil.getAccessFor(context, recipient), + ContentHint.IMPLICIT, + message); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java new file mode 100644 index 0000000000..d3c2372506 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendRetryReceiptJob.java @@ -0,0 +1,119 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.util.concurrent.TimeUnit; + +public final class SendRetryReceiptJob extends BaseJob { + + private static final String TAG = Log.tag(SendRetryReceiptJob.class); + + public static final String KEY = "SendRetryReceiptJob"; + + private static final String KEY_RECIPIENT_ID = "recipient_id"; + private static final String KEY_ERROR_MESSAGE = "error_message"; + private static final String KEY_GROUP_ID = "group_id"; + + private final RecipientId recipientId; + private final Optional groupId; + private final DecryptionErrorMessage errorMessage; + + public SendRetryReceiptJob(@NonNull RecipientId recipientId, @NonNull Optional groupId, @NonNull DecryptionErrorMessage errorMessage) { + this(recipientId, + groupId, + errorMessage, + new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue(recipientId.toQueueKey()) + .setMaxAttempts(Parameters.UNLIMITED) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .build()); + } + + private SendRetryReceiptJob(@NonNull RecipientId recipientId, + @NonNull Optional groupId, + @NonNull DecryptionErrorMessage errorMessage, + @NonNull Parameters parameters) + { + super(parameters); + this.recipientId = recipientId; + this.groupId = groupId; + this.errorMessage = errorMessage; + } + + @Override + public @NonNull Data serialize() { + Data.Builder builder = new Data.Builder() + .putString(KEY_RECIPIENT_ID, recipientId.serialize()) + .putBlobAsString(KEY_ERROR_MESSAGE, errorMessage.serialize()); + + if (groupId.isPresent()) { + builder.putBlobAsString(KEY_GROUP_ID, groupId.get().getDecodedId()); + } + + return builder.build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + protected void onRun() throws Exception { + Recipient recipient = Recipient.resolved(recipientId); + SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient); + Optional access = UnidentifiedAccessUtil.getAccessFor(context, recipient); + Optional group = groupId.transform(GroupId::getDecodedId); + + Log.i(TAG, "Sending retry receipt for " + errorMessage.getTimestamp() + " to " + recipientId + ", device: " + errorMessage.getDeviceId()); + ApplicationDependencies.getSignalServiceMessageSender().sendRetryReceipt(address, access, group, errorMessage); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof PushNetworkException; + } + + @Override + public void onFailure() { + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull SendRetryReceiptJob create(@NonNull Parameters parameters, @NonNull Data data) { + try { + RecipientId recipientId = RecipientId.from(data.getString(KEY_RECIPIENT_ID)); + DecryptionErrorMessage errorMessage = new DecryptionErrorMessage(data.getStringAsBlob(KEY_ERROR_MESSAGE)); + Optional groupId = Optional.absent(); + + if (data.hasString(KEY_GROUP_ID)) { + groupId = Optional.of(GroupId.pushOrThrow(data.getStringAsBlob(KEY_GROUP_ID))); + } + + return new SendRetryReceiptJob(recipientId, groupId, errorMessage, parameters); + } catch (InvalidMessageException e) { + throw new AssertionError(e); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java new file mode 100644 index 0000000000..4ded2905d4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SenderKeyDistributionSendJob.java @@ -0,0 +1,117 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage; +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.push.DistributionId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Sends a {@link SenderKeyDistributionMessage} to a target recipient. + * + * Will re-check group membership at send time and send the proper distribution message if they're still a member. + */ +public final class SenderKeyDistributionSendJob extends BaseJob { + + private static final String TAG = Log.tag(SenderKeyDistributionSendJob.class); + + public static final String KEY = "SenderKeyDistributionSendJob"; + + private static final String KEY_RECIPIENT_ID = "recipient_id"; + private static final String KEY_GROUP_ID = "group_id"; + + private final RecipientId recipientId; + private final GroupId.V2 groupId; + + public SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId) { + this(recipientId, groupId, new Parameters.Builder() + .setQueue(recipientId.toQueueKey()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); + } + + private SenderKeyDistributionSendJob(@NonNull RecipientId recipientId, @NonNull GroupId.V2 groupId, @NonNull Parameters parameters) { + super(parameters); + + this.recipientId = recipientId; + this.groupId = groupId; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize()) + .putBlobAsString(KEY_GROUP_ID, groupId.getDecodedId()) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + protected void onRun() throws Exception { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + + if (!groupDatabase.isCurrentMember(groupId, recipientId)) { + Log.w(TAG, recipientId + " is no longer a member of " + groupId + "! Not sending."); + return; + } + + Recipient recipient = Recipient.resolved(recipientId); + + if (recipient.getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { + Log.w(TAG, recipientId + " does not support sender key! Not sending."); + return; + } + + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + List address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, recipient)); + DistributionId distributionId = groupDatabase.getOrCreateDistributionId(groupId); + SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId); + List> access = UnidentifiedAccessUtil.getAccessFor(context, Collections.singletonList(recipient)); + + messageSender.sendSenderKeyDistributionMessage(address, access, message, groupId.getDecodedId()); + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return false; + } + + @Override + public void onFailure() { + + } + + public static final class Factory implements Job.Factory { + + @Override + public @NonNull SenderKeyDistributionSendJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new SenderKeyDistributionSendJob(RecipientId.from(data.getString(KEY_RECIPIENT_ID)), + GroupId.pushOrThrow(data.getStringAsBlob(KEY_GROUP_ID)).requireV2(), + parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java index cc69ba8908..6bab4cf3e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.net.NotPushRegisteredException; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -22,6 +23,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage.Action; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Collections; @@ -105,8 +107,8 @@ public class TypingSendJob extends BaseJob { return; } - List recipients = Collections.singletonList(recipient); - Optional groupId = Optional.absent(); + List recipients = Collections.singletonList(recipient); + Optional groupId = Optional.absent(); if (recipient.isGroup()) { recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); @@ -118,23 +120,28 @@ public class TypingSendJob extends BaseJob { .filter(r -> !r.isBlocked()) .toList()); - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients); - SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId); - - if (addresses.isEmpty()) { - Log.w(TAG, "No one to send typing indicators to"); - return; - } - - if (isCanceled()) { - Log.w(TAG, "Canceled before send!"); - return; - } + SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId); try { - messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled); + if (recipient.isPushV2Group()) { + GroupSendUtil.sendTypingMessage(context, recipient.requireGroupId().requireV2(), recipients, typingMessage, this::isCanceled); + } else { + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); + List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients); + + if (addresses.isEmpty()) { + Log.w(TAG, "No one to send typing indicators to"); + return; + } + + if (isCanceled()) { + Log.w(TAG, "Canceled before send!"); + return; + } + + messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled); + } } catch (CancelationException e) { Log.w(TAG, "Canceled during send!"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index 79a18ee0bd..f1b97f9d51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -18,6 +18,7 @@ public final class InternalValues extends SignalStoreValues { public static final String RECIPIENT_DETAILS = "internal.recipient_details"; public static final String FORCE_CENSORSHIP = "internal.force_censorship"; public static final String FORCE_BUILT_IN_EMOJI = "internal.force_built_in_emoji"; + public static final String REMOVE_SENDER_KEY_MINIMUM = "internal.remove_sender_key_minimum"; InternalValues(KeyValueStore store) { super(store); @@ -82,12 +83,19 @@ public final class InternalValues extends SignalStoreValues { } /** - * Force the app to behave as if it is in a country where Signal is censored. + * Force the app to use the emoji that ship with the app, as opposed to the ones that were downloaded. */ public synchronized boolean forceBuiltInEmoji() { return FeatureFlags.internalUser() && getBoolean(FORCE_BUILT_IN_EMOJI, false); } + /** + * Remove the requirement that there must be two sender-key-capable recipients to use sender key + */ + public synchronized boolean removeSenderKeyMinimum() { + return FeatureFlags.internalUser() && getBoolean(REMOVE_SENDER_KEY_MINIMUM, false); + } + /** * Disable initiating a GV1->GV2 auto-migration. You can still recognize a group has been * auto-migrated. diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java index ce6b2be747..5976592ec9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionCapabilities.java @@ -33,9 +33,11 @@ public final class LogSectionCapabilities implements LogSection { return new StringBuilder().append("-- Local").append("\n") .append("GV2 : ").append(capabilities.isGv2()).append("\n") .append("GV1 Migration: ").append(capabilities.isGv1Migration()).append("\n") + .append("Sender Key : ").append(capabilities.isSenderKey()).append("\n") .append("\n") .append("-- Global").append("\n") .append("GV2 : ").append(self.getGroupsV2Capability()).append("\n") - .append("GV1 Migration: ").append(self.getGroupsV1MigrationCapability()).append("\n"); + .append("GV1 Migration: ").append(self.getGroupsV1MigrationCapability()).append("\n") + .append("Sender Key : ").append(self.getSenderKeyCapability()).append("\n"); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java new file mode 100644 index 0000000000..ca38af6bc7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -0,0 +1,319 @@ +package org.thoughtcrime.securesms.messages; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.SenderKeyUtil; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.NoSessionException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.CancelationException; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.push.http.CancelationSignal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public final class GroupSendUtil { + + private static final String TAG = Log.tag(GroupSendUtil.class); + + private static final long MAX_KEY_AGE = TimeUnit.DAYS.toMillis(30); + + private GroupSendUtil() {} + + + /** + * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of + * {@link SendMessageResult}s just like we're used to. + * + * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. + */ + @WorkerThread + public static List sendDataMessage(@NonNull Context context, + @NonNull GroupId.V2 groupId, + @NonNull List allTargets, + boolean isRecipientUpdate, + ContentHint contentHint, + @NonNull SignalServiceDataMessage message) + throws IOException, UntrustedIdentityException + { + return sendMessage(context, groupId, allTargets, isRecipientUpdate, new DataSendOperation(message, contentHint), null); + } + + /** + * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of + * {@link SendMessageResult}s just like we're used to. + */ + @WorkerThread + public static List sendTypingMessage(@NonNull Context context, + @NonNull GroupId.V2 groupId, + @NonNull List allTargets, + @NonNull SignalServiceTypingMessage message, + @Nullable CancelationSignal cancelationSignal) + throws IOException, UntrustedIdentityException + { + return sendMessage(context, groupId, allTargets, false, new TypingSendOperation(message), cancelationSignal); + } + + /** + * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of + * {@link SendMessageResult}s just like we're used to. + * + * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. + */ + @WorkerThread + private static List sendMessage(@NonNull Context context, + @NonNull GroupId.V2 groupId, + @NonNull List allTargets, + boolean isRecipientUpdate, + @NonNull SendOperation sendOperation, + @Nullable CancelationSignal cancelationSignal) + throws IOException, UntrustedIdentityException + { + RecipientData recipients = new RecipientData(context, allTargets); + + List senderKeyTargets = new LinkedList<>(); + List legacyTargets = new LinkedList<>(); + + for (Recipient recipient : allTargets) { + Optional access = recipients.getAccessPair(recipient.getId()); + + if (recipient.getSenderKeyCapability() == Recipient.Capability.SUPPORTED && + recipient.hasUuid() && + access.isPresent() && + access.get().getTargetUnidentifiedAccess().isPresent()) + { + senderKeyTargets.add(recipient); + } else { + legacyTargets.add(recipient); + } + } + + if (FeatureFlags.senderKey()) { + if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { + Log.i(TAG, "All of our devices do not support sender key. Using legacy."); + legacyTargets.addAll(senderKeyTargets); + senderKeyTargets.clear(); + } else if (SignalStore.internalValues().removeSenderKeyMinimum()) { + Log.i(TAG, "Sender key minimum removed. Using for " + senderKeyTargets.size() + " recipients."); + } else if (senderKeyTargets.size() < 2) { + Log.i(TAG, "Too few sender-key-capable users (" + senderKeyTargets.size() + "). Doing all legacy sends."); + legacyTargets.addAll(senderKeyTargets); + senderKeyTargets.clear(); + } else { + Log.i(TAG, "Can use sender key for " + senderKeyTargets.size() + "/" + allTargets.size() + " recipients."); + } + } else { + Log.i(TAG, "Feature flag disabled. Using legacy."); + legacyTargets.addAll(senderKeyTargets); + senderKeyTargets.clear(); + } + + List allResults = new ArrayList<>(allTargets.size()); + SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); + DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId); + + if (senderKeyTargets.size() > 0) { + long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId); + long keyAge = System.currentTimeMillis() - keyCreateTime; + + if (keyCreateTime != -1 && keyAge > MAX_KEY_AGE) { + Log.w(TAG, "Key is " + (keyAge) + " ms old (~" + TimeUnit.MILLISECONDS.toDays(keyAge) + " days). Rotating."); + SenderKeyUtil.rotateOurKey(context, distributionId); + } + + try { + List targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); + List access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList()); + List results = sendOperation.sendWithSenderKey(messageSender, distributionId, targets, access, isRecipientUpdate); + + allResults.addAll(results); + + int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count(); + Log.d(TAG, "Successfully sent using sender key to " + successCount + "/" + targets.size() + " sender key targets."); + } catch (NoSessionException e) { + Log.w(TAG, "No session. Falling back to legacy sends.", e); + legacyTargets.addAll(senderKeyTargets); + } catch (InvalidKeyException e) { + Log.w(TAG, "Invalid Key. Falling back to legacy sends.", e); + legacyTargets.addAll(senderKeyTargets); + } + } + + if (cancelationSignal != null && cancelationSignal.isCanceled()) { + throw new CancelationException(); + } + + if (legacyTargets.size() > 0) { + Log.i(TAG, "Need to do " + legacyTargets.size() + " legacy sends."); + + List targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList()); + List> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList()); + boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0; + + List results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, cancelationSignal); + + allResults.addAll(results); + + int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count(); + Log.d(TAG, "Successfully using 1:1 to " + successCount + "/" + targets.size() + " legacy targets."); + } + + return allResults; + } + + /** Abstraction layer to handle the different types of message send operations we can do */ + private interface SendOperation { + @NonNull List sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender, + @NonNull DistributionId distributionId, + @NonNull List targets, + @NonNull List access, + boolean isRecipientUpdate) + throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException; + + @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, + @NonNull List targets, + @NonNull List> access, + boolean isRecipientUpdate, + @Nullable CancelationSignal cancelationSignal) + throws IOException, UntrustedIdentityException; + } + + private static class DataSendOperation implements SendOperation { + private final SignalServiceDataMessage message; + private final ContentHint contentHint; + + private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) { + this.message = message; + this.contentHint = contentHint; + } + + @Override + public @NonNull List sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender, + @NonNull DistributionId distributionId, + @NonNull List targets, + @NonNull List access, + boolean isRecipientUpdate) + throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException + { + return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message); + } + + @Override + public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, + @NonNull List targets, + @NonNull List> access, + boolean isRecipientUpdate, + @Nullable CancelationSignal cancelationSignal) + throws IOException, UntrustedIdentityException + { + return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message); + } + } + + private static class TypingSendOperation implements SendOperation { + + private final SignalServiceTypingMessage message; + + private TypingSendOperation(@NonNull SignalServiceTypingMessage message) { + this.message = message; + } + + @Override + public @NonNull List sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender, + @NonNull DistributionId distributionId, + @NonNull List targets, + @NonNull List access, + boolean isRecipientUpdate) + throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException + { + messageSender.sendGroupTyping(distributionId, targets, access, message); + return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList()); + } + + @Override + public @NonNull List sendLegacy(@NonNull SignalServiceMessageSender messageSender, + @NonNull List targets, + @NonNull List> access, + boolean isRecipientUpdate, + @Nullable CancelationSignal cancelationSignal) + throws IOException + { + messageSender.sendTyping(targets, access, message, cancelationSignal); + return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList()); + } + } + + /** + * Little utility wrapper that lets us get the various different slices of recipient models that we need for different methods. + */ + private static final class RecipientData { + + private final Map> accessById; + private final Map addressById; + + RecipientData(@NonNull Context context, @NonNull List recipients) throws IOException { + this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients); + this.addressById = mapAddresses(context, recipients); + } + + @NonNull SignalServiceAddress getAddress(@NonNull RecipientId id) { + return Objects.requireNonNull(addressById.get(id)); + } + + @NonNull Optional getAccessPair(@NonNull RecipientId id) { + return Objects.requireNonNull(accessById.get(id)); + } + + @NonNull UnidentifiedAccess requireAccess(@NonNull RecipientId id) { + return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get(); + } + + private static @NonNull Map mapAddresses(@NonNull Context context, @NonNull List recipients) throws IOException { + List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); + + Iterator recipientIterator = recipients.iterator(); + Iterator addressIterator = addresses.iterator(); + + Map map = new HashMap<>(recipients.size()); + + while (recipientIterator.hasNext()) { + map.put(recipientIterator.next().getId(), addressIterator.next()); + } + + return map; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java index cfdaf3b975..df0fba5390 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/IncomingMessageProcessor.java @@ -91,7 +91,7 @@ public class IncomingMessageProcessor { if (envelope.isReceipt()) { processReceipt(envelope); return null; - } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender()) { + } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isPlaintextContent()) { return processMessage(envelope); } else { Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 2399477d5c..9d55949284 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -67,12 +68,14 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob; import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob; import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob; import org.thoughtcrime.securesms.jobs.ProfileKeySendJob; +import org.thoughtcrime.securesms.jobs.PushGroupSendJob; import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; +import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob; import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -103,6 +106,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.storage.StorageSyncHelper; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.IdentityUtil; @@ -110,9 +114,13 @@ import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage; import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -142,6 +150,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.payments.Money; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.io.IOException; @@ -219,6 +228,10 @@ public final class MessageContentProcessor { log(String.valueOf(content.getTimestamp()), "Beginning message processing."); + if (content.getSenderKeyDistributionMessage().isPresent()) { + handleSenderKeyDistributionMessage(content.getSender(), content.getSenderDevice(), content.getSenderKeyDistributionMessage().get()); + } + if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent(); @@ -328,6 +341,8 @@ public final class MessageContentProcessor { else if (message.isViewedReceipt()) handleViewedReceipt(content, message); } else if (content.getTypingMessage().isPresent()) { handleTypingMessage(content, content.getTypingMessage().get()); + } else if (content.getDecryptionErrorMessage().isPresent()) { + handleRetryReceipt(content, content.getDecryptionErrorMessage().get()); } else { warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!"); } @@ -1549,6 +1564,12 @@ public final class MessageContentProcessor { } } + private void handleSenderKeyDistributionMessage(@NonNull SignalServiceAddress address, int deviceId, @NonNull SenderKeyDistributionMessage message) { + log("Processing SenderKeyDistributionMessage."); + SignalServiceMessageSender sender = ApplicationDependencies.getSignalServiceMessageSender(); + sender.processSenderKeyDistributionMessage(new SignalProtocolAddress(address.getIdentifier(), deviceId), message); + } + private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { @@ -1657,6 +1678,81 @@ public final class MessageContentProcessor { } } + private void handleRetryReceipt(@NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) { + if (!FeatureFlags.senderKey()) { + Log.w(TAG, "Sender key not enabled, skipping retry receipt."); + return; + } + + Recipient requester = Recipient.externalHighTrustPush(context, content.getSender()); + long sentTimestamp = decryptionErrorMessage.getTimestamp(); + + if (!requester.hasUuid()) { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester " + requester.getId() + " somehow has no UUID! timestamp: " + sentTimestamp); + return; + } + + MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId()); + + if (messageRecord == null) { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find message for " + requester.getId() + " with timestamp " + sentTimestamp); + // TODO Send distribution message? + return; + } + + Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(messageRecord.getThreadId()); + + if (threadRecipient == null) { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find a recipient for thread " + messageRecord.getThreadId()); + return; + } + + if (messageRecord.isMms()) { + log(String.valueOf(content.getTimestamp()), "[RetryReceipt] MMS " + messageRecord.getId()); + MmsMessageRecord mms = (MmsMessageRecord) messageRecord; + + if (threadRecipient.isPushV2Group()) { + DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(threadRecipient.requireGroupId().requireV2()); + SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId()); + + DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress)); + + GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + GroupReceiptInfo receiptInfo = receiptDatabase.getGroupReceiptInfo(mms.getId(), requester.getId()); + boolean needsDistributionMessage = true; + + if (receiptInfo == null) { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester was never sent message " + mms.getId() + "! Cannot resend it."); + } else if (receiptInfo.getStatus() >= GroupReceiptDatabase.STATUS_DELIVERED) { + log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully delivered to the requester. Not resending."); + } else { + long messageAge = System.currentTimeMillis() - mms.getDateSent(); + + if (messageAge < FeatureFlags.retryRespondMaxAge()) { + log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. Resending."); + + DatabaseFactory.getGroupReceiptDatabase(context).update(requester.getId(), mms.getId(), GroupReceiptDatabase.STATUS_UNDELIVERED, System.currentTimeMillis()); + ApplicationDependencies.getJobManager().startChain(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2())) + .then(new PushGroupSendJob(mms.getId(), threadRecipient.getId(), requester.getId(), false)) + .enqueue(); + + needsDistributionMessage = false; + } else { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. But it's " + messageAge + " ms old, so we're not resending."); + } + } + + if (needsDistributionMessage && threadRecipient.getParticipants().contains(requester)) { + warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester is, however, in the group now. Sending distribution message."); + ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2())); + } + } + } else { + log(String.valueOf(content.getTimestamp()), "[RetryReceipt] SMS " + messageRecord.getId()); + SmsMessageRecord sms = (SmsMessageRecord) messageRecord; + } + } + private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) { if (message.isViewOnce()) { List attachments = message.getAttachments().or(Collections.emptyList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java index a220608ca7..fc10194cc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptionUtil.java @@ -21,25 +21,30 @@ import org.signal.libsignal.metadata.SelfSendException; import org.thoughtcrime.securesms.crypto.DatabaseSessionLock; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.BadGroupIdException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; +import org.thoughtcrime.securesms.jobs.SendRetryReceiptJob; import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata; import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; -import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -77,9 +82,16 @@ public final class MessageDecryptionUtil { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); return DecryptionResult.forError(MessageState.INVALID_VERSION, toExceptionMetadata(e), jobs); - } catch (ProtocolInvalidMessageException | ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException e) { + } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolUntrustedIdentityException | ProtocolNoSessionException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); - jobs.add(new AutomaticSessionResetJob(Recipient.external(context, e.getSender()).getId(), e.getSenderDevice(), envelope.getTimestamp())); + Recipient sender = Recipient.external(context, e.getSender()); + + if (sender.supportsMessageRetries() && Recipient.self().supportsMessageRetries() && FeatureFlags.senderKey()) { + jobs.add(handleRetry(context, sender, envelope, e)); + } else { + jobs.add(new AutomaticSessionResetJob(sender.getId(), e.getSenderDevice(), envelope.getTimestamp())); + } + return DecryptionResult.forNoop(jobs); } catch (ProtocolLegacyMessageException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); @@ -87,7 +99,7 @@ public final class MessageDecryptionUtil { } catch (ProtocolDuplicateMessageException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); return DecryptionResult.forError(MessageState.DUPLICATE_MESSAGE, toExceptionMetadata(e), jobs); - } catch (InvalidMetadataVersionException | InvalidMetadataMessageException e) { + } catch (InvalidMetadataVersionException | InvalidMetadataMessageException | ProtocolInvalidMessageException e) { Log.w(TAG, String.valueOf(envelope.getTimestamp()), e); return DecryptionResult.forNoop(jobs); } catch (SelfSendException e) { @@ -103,6 +115,62 @@ public final class MessageDecryptionUtil { } } + private static @NonNull Job handleRetry(@NonNull Context context, @NonNull Recipient sender, @NonNull SignalServiceEnvelope envelope, @NonNull ProtocolException protocolException) { + ContentHint contentHint = ContentHint.fromType(protocolException.getContentHint()); + int senderDevice = protocolException.getSenderDevice(); + long receivedTimestamp = System.currentTimeMillis(); + Optional groupId = Optional.absent(); + + if (protocolException.getGroupId().isPresent()) { + try { + groupId = Optional.of(GroupId.push(protocolException.getGroupId().get())); + } catch (BadGroupIdException e) { + Log.w(TAG, "[" + envelope.getTimestamp() + "] Bad groupId!"); + } + } + + Log.w(TAG, "[" + envelope.getTimestamp() + "] Could not decrypt a message with a type of " + contentHint); + + long threadId; + + if (groupId.isPresent()) { + Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId.get()); + threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + } else { + threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender); + } + + switch (contentHint) { + case DEFAULT: + Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting an error right away because it's " + contentHint); + DatabaseFactory.getSmsDatabase(context).insertBadDecryptMessage(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId); + break; + case RESENDABLE: + Log.w(TAG, "[" + envelope.getTimestamp() + "] Inserting into pending retries store because it's " + contentHint); + DatabaseFactory.getPendingRetryReceiptDatabase(context).insert(sender.getId(), senderDevice, envelope.getTimestamp(), receivedTimestamp, threadId); + ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary(); + break; + case IMPLICIT: + Log.w(TAG, "[" + envelope.getTimestamp() + "] Not inserting any error because it's " + contentHint); + break; + } + + byte[] originalContent; + int envelopeType; + if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) { + originalContent = protocolException.getUnidentifiedSenderMessageContent().get().getContent(); + envelopeType = protocolException.getUnidentifiedSenderMessageContent().get().getType(); + } else { + originalContent = envelope.getContent(); + envelopeType = envelope.getType(); + } + + DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, envelopeType, envelope.getTimestamp(), senderDevice); + + return new SendRetryReceiptJob(sender.getId(), groupId, decryptionErrorMessage); + } + + private static ExceptionMetadata toExceptionMetadata(@NonNull UnsupportedDataMessageException e) throws NoSenderException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 7c62a4180e..98b3caa2e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -40,7 +40,7 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 34; + public static final int CURRENT_VERSION = 35; private static final class Version { static final int LEGACY = 1; @@ -76,6 +76,7 @@ public class ApplicationMigrations { static final int PROFILE_SHARING_UPDATE = 32; static final int SMS_STORAGE_SYNC = 33; static final int APPLY_UNIVERSAL_EXPIRE = 34; + static final int SENDER_KEY = 35; } /** @@ -322,6 +323,10 @@ public class ApplicationMigrations { jobs.put(Version.SMS_STORAGE_SYNC, new ApplyUnknownFieldsToSelfMigrationJob()); } + if (lastSeenVersion < Version.SENDER_KEY) { + jobs.put(Version.SENDER_KEY, new AttributesMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index fee47985a3..43440d76b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -104,6 +104,7 @@ public class Recipient { private final boolean forceSmsSelection; private final Capability groupsV2Capability; private final Capability groupsV1MigrationCapability; + private final Capability senderKeyCapability; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -352,6 +353,7 @@ public class Recipient { this.forceSmsSelection = false; this.groupsV2Capability = Capability.UNKNOWN; this.groupsV1MigrationCapability = Capability.UNKNOWN; + this.senderKeyCapability = Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; @@ -402,6 +404,7 @@ public class Recipient { this.forceSmsSelection = details.forceSmsSelection; this.groupsV2Capability = details.groupsV2Capability; this.groupsV1MigrationCapability = details.groupsV1MigrationCapability; + this.senderKeyCapability = details.senderKeyCapability; this.storageId = details.storageId; this.mentionSetting = details.mentionSetting; this.wallpaper = details.wallpaper; @@ -867,6 +870,17 @@ public class Recipient { return groupsV1MigrationCapability; } + public @NonNull Capability getSenderKeyCapability() { + return senderKeyCapability; + } + + /** + * True if this recipient supports the message retry system, or false if we should use the legacy session reset system. + */ + public boolean supportsMessageRetries() { + return getSenderKeyCapability() == Capability.SUPPORTED; + } + public @Nullable byte[] getProfileKey() { return profileKey; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index af6da59740..0a6a9aada8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -63,6 +63,7 @@ public class RecipientDetails { final boolean forceSmsSelection; final Recipient.Capability groupsV2Capability; final Recipient.Capability groupsV1MigrationCapability; + final Recipient.Capability senderKeyCapability; final InsightsBannerTier insightsBannerTier; final byte[] storageId; final MentionSetting mentionSetting; @@ -117,6 +118,7 @@ public class RecipientDetails { this.forceSmsSelection = settings.isForceSmsSelection(); this.groupsV2Capability = settings.getGroupsV2Capability(); this.groupsV1MigrationCapability = settings.getGroupsV1MigrationCapability(); + this.senderKeyCapability = settings.getSenderKeyCapability(); this.insightsBannerTier = settings.getInsightsBannerTier(); this.storageId = settings.getStorageId(); this.mentionSetting = settings.getMentionSetting(); @@ -171,6 +173,7 @@ public class RecipientDetails { this.groupName = null; this.groupsV2Capability = Recipient.Capability.UNKNOWN; this.groupsV1MigrationCapability = Recipient.Capability.UNKNOWN; + this.senderKeyCapability = Recipient.Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index c0ab709ddd..3442fdbad5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.AppCapabilities; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.PreKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.SessionUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.IdentityDatabase; @@ -203,6 +204,7 @@ public final class CodeVerificationRequest { TextSecurePreferences.setLocalRegistrationId(context, registrationId); SessionUtil.archiveAllSessions(context); + SenderKeyUtil.clearAllState(context); SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword()); KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()) : null; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/PendingRetryReceiptManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/PendingRetryReceiptManager.java new file mode 100644 index 0000000000..bd0e01b51d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/PendingRetryReceiptManager.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.service; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageDatabase; +import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase; +import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.FeatureFlags; + + +/** + * Manages the time-based creation of error messages for retries that are pending for messages we couldn't decrypt. + */ +public final class PendingRetryReceiptManager extends TimedEventManager { + + private static final String TAG = Log.tag(PendingRetryReceiptManager.class); + + private final PendingRetryReceiptDatabase pendingDatabase; + private final MessageDatabase messageDatabase; + + public PendingRetryReceiptManager(@NonNull Application application) { + super(application, "PendingRetryReceiptManager"); + + this.pendingDatabase = DatabaseFactory.getPendingRetryReceiptDatabase(application); + this.messageDatabase = DatabaseFactory.getSmsDatabase(application); + + scheduleIfNecessary(); + } + + @WorkerThread + @Override + protected @Nullable PendingRetryReceiptModel getNextClosestEvent() { + PendingRetryReceiptModel model = pendingDatabase.getOldest(); + + if (model != null) { + Log.i(TAG, "Next closest expiration is in " + getDelayForEvent(model) + " ms for timestamp " + model.getSentTimestamp() + "."); + } else { + Log.d(TAG, "No pending receipts to schedule."); + } + + return model; + } + + @WorkerThread + @Override + protected void executeEvent(@NonNull PendingRetryReceiptModel event) { + Log.w(TAG, "It's been " + (System.currentTimeMillis() - event.getReceivedTimestamp()) + " ms since this retry receipt was received. Showing an error."); + messageDatabase.insertBadDecryptMessage(event.getAuthor(), event.getAuthorDevice(), event.getSentTimestamp(), event.getReceivedTimestamp(), event.getThreadId()); + pendingDatabase.delete(event.getId()); + } + + @WorkerThread + @Override + protected long getDelayForEvent(@NonNull PendingRetryReceiptModel event) { + long expiresAt = event.getReceivedTimestamp() + FeatureFlags.retryReceiptLifespan(); + long timeLeft = expiresAt - System.currentTimeMillis(); + + return Math.max(0, timeLeft); + } + + @AnyThread + @Override + protected void scheduleAlarm(@NonNull Application application, long delay) { + setAlarm(application, delay, PendingRetryReceiptAlarm.class); + } + + public static class PendingRetryReceiptAlarm extends BroadcastReceiver { + + private static final String TAG = Log.tag(PendingRetryReceiptAlarm.class); + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive()"); + ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index de75167a86..9221aaeaca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -82,6 +82,9 @@ public final class FeatureFlags { private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels"; private static final String GROUPS_V2_DESCRIPTION_VERSION = "android.groupsv2.descriptionVersion"; private static final String DEFAULT_MESSAGE_TIMER = "android.defaultMessageTimer.2"; + private static final String RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan"; + private static final String RETRY_RESPOND_MAX_AGE = "android.retryRespondMaxAge"; + private static final String SENDER_KEY = "android.senderKey"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -117,7 +120,10 @@ public final class FeatureFlags { MP4_GIF_SEND_SUPPORT, MEDIA_QUALITY_LEVELS, GROUPS_V2_DESCRIPTION_VERSION, - DEFAULT_MESSAGE_TIMER + DEFAULT_MESSAGE_TIMER, + RETRY_RECEIPT_LIFESPAN, + RETRY_RESPOND_MAX_AGE, + SENDER_KEY ); @VisibleForTesting @@ -165,7 +171,9 @@ public final class FeatureFlags { MP4_GIF_SEND_SUPPORT, MEDIA_QUALITY_LEVELS, GROUPS_V2_DESCRIPTION_VERSION, - DEFAULT_MESSAGE_TIMER + DEFAULT_MESSAGE_TIMER, + RETRY_RECEIPT_LIFESPAN, + RETRY_RESPOND_MAX_AGE ); /** @@ -373,6 +381,21 @@ public final class FeatureFlags { return getBoolean(DEFAULT_MESSAGE_TIMER, false); } + /** How long to wait before considering a retry to be a failure. */ + public static long retryReceiptLifespan() { + return getLong(RETRY_RECEIPT_LIFESPAN, TimeUnit.HOURS.toMillis(1)); + } + + /** How old a message is allowed to be while still resending in response to a retry receipt . */ + public static long retryRespondMaxAge() { + return getLong(RETRY_RESPOND_MAX_AGE, TimeUnit.DAYS.toMillis(1)); + } + + /** Whether or not sending using sender key is enabled. */ + public static boolean senderKey() { + return getBoolean(SENDER_KEY, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); @@ -560,6 +583,24 @@ public final class FeatureFlags { return defaultValue; } + private static long getLong(@NonNull String key, long defaultValue) { + Long forced = (Long) FORCED_VALUES.get(key); + if (forced != null) { + return forced; + } + + Object remote = REMOTE_VALUES.get(key); + if (remote instanceof String) { + try { + return Long.parseLong((String) remote); + } catch (NumberFormatException e) { + Log.w(TAG, "Expected a long for key '" + key + "', but got something else! Falling back to the default."); + } + } + + return defaultValue; + } + private static String getString(@NonNull String key, String defaultValue) { String forced = (String) FORCED_VALUES.get(key); if (forced != null) { diff --git a/app/src/main/res/drawable/chat_session_refresh.xml b/app/src/main/res/drawable/chat_session_refresh.xml new file mode 100644 index 0000000000..1f349a97b6 --- /dev/null +++ b/app/src/main/res/drawable/chat_session_refresh.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bad_decrypt_learn_more_dialog_fragment.xml b/app/src/main/res/layout/bad_decrypt_learn_more_dialog_fragment.xml new file mode 100644 index 0000000000..c0a2dc66e3 --- /dev/null +++ b/app/src/main/res/layout/bad_decrypt_learn_more_dialog_fragment.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ 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 697b87cbc1..d4e58488f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1212,6 +1212,7 @@ You marked your safety number with %s verified from another device You marked your safety number with %s unverified You marked your safety number with %s unverified from another device + A message from %s couldn\'t be delivered %1$s started a group call · %2$s @@ -1625,6 +1626,7 @@ You marked verified You marked unverified Message could not be processed + Delivery issue Message Request Photo GIF @@ -2071,6 +2073,11 @@ Signal requires MMS settings to deliver media and group messages through your wireless carrier. Your device does not make this information available, which is occasionally true for locked devices and other restrictive configurations. To send media and group messages, tap \'OK\' and complete the requested settings. The MMS settings for your carrier can generally be located by searching for \'your carrier APN\'. You will only need to do this once. + + Delivery Issue + A message, sticker, reaction, or read receipt couldn\'t be delivered to you from %s. They may have tried sending it to you directly, or in a group. + A message, sticker, reaction, or read receipt couldn\'t be delivered to you from %s. + First name (required) Last name (optional) @@ -2464,6 +2471,13 @@ Delete all dynamic shortcuts Click to delete all dynamic shortcuts Disable Profile Sharing + Sender Key + Clear all state + Click to delete all sender key state + Clear shared state + Click to delete all sharing state + Remove 2 person minimum + Remove the requirement that you need at least 2 recipients to use sender key. All activity diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index ed2deffc5c..334e20514e 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -3,6 +3,9 @@ dependencyVerification { verify = [ + ['androidx.activity:activity-ktx:1.1.0', + '1996c36d3d2d62db5020b8ec634b5f854b1a698960c3552e1a00c69221baeabe'], + ['androidx.activity:activity:1.1.0', '4f2b35916768032f7d0c20e250e28b29037ed4ce9ebf3da4fcd51bcb0c6067ef'], @@ -48,6 +51,9 @@ dependencyVerification { ['androidx.cardview:cardview:1.0.0', '1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'], + ['androidx.collection:collection-ktx:1.1.0', + '2bfc54475c047131913361f56d0f7f019c6e5bee53eeb0eb7d94a7c499a05227'], + ['androidx.collection:collection:1.1.0', '632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'], @@ -63,6 +69,9 @@ dependencyVerification { ['androidx.coordinatorlayout:coordinatorlayout:1.1.0', '44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb'], + ['androidx.core:core-ktx:1.1.0', + '070cc5f8864f449128a2f4b25ca5b67aa3adca3ee1bd611e2eaf1a18fad83178'], + ['androidx.core:core:1.3.2', '94de196cd67950cff6ef3e1ac59015f8eaaf61840bdc238f2cf54ddef8dd0be9'], @@ -84,6 +93,9 @@ dependencyVerification { ['androidx.exifinterface:exifinterface:1.0.0', 'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'], + ['androidx.fragment:fragment-ktx:1.2.5', + '50f0f3b734f93829eeac7456b7cb13e5430741e555c535911a958ee4a8242bca'], + ['androidx.fragment:fragment:1.2.5', 'd19e82d142def6c4e136da70bf92f194c0ecc61d14ab4e84567b2ced0920fa93'], @@ -117,6 +129,9 @@ dependencyVerification { ['androidx.lifecycle:lifecycle-extensions:2.1.0', 'bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d'], + ['androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0', + '5951f882e95b7e05ceb9adfca0fa2ebd511d63ea5a00da4eae6c6d0c1903da18'], + ['androidx.lifecycle:lifecycle-livedata-core:2.2.0', '556c1f3af90aa9d7d0d330565adbf6da71b2429148bac91e07c485f4f9abf614'], @@ -126,12 +141,18 @@ dependencyVerification { ['androidx.lifecycle:lifecycle-process:2.1.0', '8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac'], + ['androidx.lifecycle:lifecycle-runtime-ktx:2.2.0', + 'c29fc87694e6ce116b61207221e53ed285862a6628055790b0bcf9ce45d8cc68'], + ['androidx.lifecycle:lifecycle-runtime:2.2.0', '2f866c07a1f33a8c9bb69a9545d4f20b4f0628cd0a155432386d7cb081e1e0bc'], ['androidx.lifecycle:lifecycle-service:2.1.0', '23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69'], + ['androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0', + 'f791001f2211947e56ad3d96d12c9ae93fc5589b88f08603f69a2265c9a7d702'], + ['androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0', '3ce866fb822b20fe2f188f974992869a0a6233fe40acbefcff090d6def5e7f33'], @@ -474,6 +495,12 @@ dependencyVerification { ['org.jetbrains.kotlin:kotlin-stdlib:1.4.32', '13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba'], + ['org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0', + 'c80aaadf041f044d324a19a73f88879dfd1e4d026b14e3230075ff9081942ae3'], + + ['org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0', + '6f3a60fea2403b80385b399952aeb3a4cf0985a45b8da04b6f31825171901a1d'], + ['org.jetbrains:annotations:13.0', 'ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478'], @@ -510,11 +537,11 @@ dependencyVerification { ['org.threeten:threetenbp:1.3.6', 'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'], - ['org.whispersystems:signal-client-android:0.5.1', - 'c5b523163612fedcf71a5fb51a85cb5a4f545329ec6cf7d4fc885e830466c2e3'], + ['org.whispersystems:signal-client-android:0.8.0', + 'f18e705717d463ecbaae3dedf0747b1fabfcfc98b708cc309200960678f230cc'], - ['org.whispersystems:signal-client-java:0.5.1', - '682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'], + ['org.whispersystems:signal-client-java:0.8.1', + '6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'], ['pl.tajchert:waitingdots:0.1.0', '2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'], diff --git a/device-transfer/lib/build.gradle b/device-transfer/lib/build.gradle index d76925b9a3..e7116d0bbf 100644 --- a/device-transfer/lib/build.gradle +++ b/device-transfer/lib/build.gradle @@ -28,7 +28,7 @@ dependencyVerification { dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation project(':core-util') - implementation 'org.whispersystems:signal-client-java:0.5.1' + implementation 'org.whispersystems:signal-client-java:0.8.1' api 'org.greenrobot:eventbus:3.0.0' testImplementation 'junit:junit:4.12' diff --git a/device-transfer/lib/witness-verifications.gradle b/device-transfer/lib/witness-verifications.gradle index e597f7e91f..6825614a2f 100644 --- a/device-transfer/lib/witness-verifications.gradle +++ b/device-transfer/lib/witness-verifications.gradle @@ -81,7 +81,7 @@ dependencyVerification { ['org.greenrobot:eventbus:3.0.0', '180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'], - ['org.whispersystems:signal-client-java:0.5.1', - '682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'], + ['org.whispersystems:signal-client-java:0.8.1', + '6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'], ] } diff --git a/libsignal/service/build.gradle b/libsignal/service/build.gradle index 5739440553..5727eb7b65 100644 --- a/libsignal/service/build.gradle +++ b/libsignal/service/build.gradle @@ -21,7 +21,7 @@ dependencies { api 'com.googlecode.libphonenumber:libphonenumber:8.12.17' api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2' - api 'org.whispersystems:signal-client-java:0.5.1' + implementation 'org.whispersystems:signal-client-java:0.8.1' api 'com.squareup.okhttp3:okhttp:3.12.10' implementation 'org.threeten:threetenbp:1.3.6' diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java index cc133bfc33..9f0b38a965 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java @@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; +import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse; import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList; import org.whispersystems.signalservice.internal.push.ProofRequiredResponse; import org.whispersystems.signalservice.internal.push.SendMessageResponse; @@ -46,6 +47,7 @@ import java.io.IOException; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -175,6 +177,33 @@ public class SignalServiceMessagePipe { } } + public Future sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online) throws IOException { + List headers = new LinkedList() {{ + add("content-type:application/vnd.signal-messenger.mrm"); + add("Unidentified-Access-Key:" + Base64.encodeBytes(joinedUnidentifiedAccess)); + }}; + + String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s", timestamp, online); + + WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() + .setId(new SecureRandom().nextLong()) + .setVerb("PUT") + .setPath(path) + .addAllHeaders(headers) + .setBody(ByteString.copyFrom(body)) + .build(); + + ListenableFuture response = websocket.sendRequest(requestMessage); + + return FutureTransformers.map(response, value -> { + if (value.getStatus() == 200) { + return JsonUtil.fromJson(value.getBody(), SendGroupMessageResponse.class); + } else { + throw new NonSuccessfulResponseCodeException(value.getStatus()); + } + }); + } + public Future send(OutgoingPushMessageList list, Optional unidentifiedAccess) throws IOException { List headers = new LinkedList() {{ add("content-type:application/json"); 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 88212129b2..89f70c435f 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 @@ -8,15 +8,26 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import org.signal.libsignal.metadata.certificate.SenderCertificate; +import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SessionBuilder; import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.GroupSessionBuilder; import org.whispersystems.libsignal.logging.Log; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.protocol.PlaintextContent; +import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage; import org.whispersystems.libsignal.state.PreKeyBundle; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.libsignal.util.guava.Preconditions; import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream; +import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.EnvelopeContent; +import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; @@ -28,6 +39,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; @@ -51,6 +63,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; @@ -62,14 +75,17 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Uint64RangeException; import org.whispersystems.signalservice.api.util.Uint64Util; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; +import org.whispersystems.signalservice.internal.push.GroupMismatchedDevices; +import org.whispersystems.signalservice.internal.push.GroupStaleDevices; +import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse; import org.whispersystems.signalservice.internal.push.MismatchedDevices; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList; -import org.whispersystems.signalservice.internal.push.ProofRequiredResponse; import org.whispersystems.signalservice.internal.push.ProvisioningProtos; import org.whispersystems.signalservice.internal.push.PushAttachmentData; import org.whispersystems.signalservice.internal.push.PushServiceSocket; @@ -86,6 +102,8 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMe import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified; import org.whispersystems.signalservice.internal.push.StaleDevices; +import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException; +import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException; import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory; @@ -94,17 +112,20 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.util.Base64; +import org.whispersystems.util.ByteArrayUtil; import org.whispersystems.util.FlagUtil; import java.io.IOException; import java.io.InputStream; import java.security.SecureRandom; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -114,6 +135,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; /** * The main interface for sending Signal Service messages. @@ -205,9 +227,26 @@ public class SignalServiceMessageSender { SignalServiceReceiptMessage message) throws IOException, UntrustedIdentityException { - byte[] content = createReceiptContent(message); + Content content = createReceiptContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, null); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null); + } + + /** + * Send a retry receipt for a bad-encrypted envelope. + */ + public void sendRetryReceipt(SignalServiceAddress recipient, + Optional unidentifiedAccess, + Optional groupId, + DecryptionErrorMessage errorMessage) + throws IOException, UntrustedIdentityException + + { + PlaintextContent content = new PlaintextContent(errorMessage); + EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId); + + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null); } /** @@ -223,9 +262,10 @@ public class SignalServiceMessageSender { SignalServiceTypingMessage message) throws IOException, UntrustedIdentityException { - byte[] content = createTypingContent(message); + Content content = createTypingContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, null); + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null); } public void sendTyping(List recipients, @@ -234,8 +274,23 @@ public class SignalServiceMessageSender { CancelationSignal cancelationSignal) throws IOException { - byte[] content = createTypingContent(message); - sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, cancelationSignal); + Content content = createTypingContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, cancelationSignal); + } + + /** + * Send a typing indicator a group. Doesn't bother with return results, since these are best-effort. + */ + public void sendGroupTyping(DistributionId distributionId, + List recipients, + List unidentifiedAccess, + SignalServiceTypingMessage message) + throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException + { + Content content = createTypingContent(message); + sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId().orNull(), false); } @@ -251,8 +306,10 @@ public class SignalServiceMessageSender { SignalServiceCallMessage message) throws IOException, UntrustedIdentityException { - byte[] content = createCallContent(message); - sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null); + Content content = createCallContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.absent()); + + sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null); } /** @@ -277,18 +334,22 @@ public class SignalServiceMessageSender { * @throws UntrustedIdentityException * @throws IOException */ - public SendMessageResult sendMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - SignalServiceDataMessage message) + public SendMessageResult sendDataMessage(SignalServiceAddress recipient, + Optional unidentifiedAccess, + ContentHint contentHint, + SignalServiceDataMessage message) throws UntrustedIdentityException, IOException { - byte[] content = createMessageContent(message); - long timestamp = message.getTimestamp(); - SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null); + Content content = createMessageContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); + long timestamp = message.getTimestamp(); + SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null); if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) { - byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false); - sendMessage(localAddress, Optional.absent(), timestamp, syncMessage, false, null); + Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false); + EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null); } // TODO [greyson][session] Delete this when we delete the button @@ -309,21 +370,81 @@ public class SignalServiceMessageSender { } /** - * Send a message to a group. + * Gives you a {@link SenderKeyDistributionMessage} that can then be sent out to recipients to tell them about your sender key. + * Will create a sender key session for the provided DistributionId if one doesn't exist. + */ + public SenderKeyDistributionMessage getOrCreateNewGroupSession(DistributionId distributionId) { + SignalProtocolAddress self = new SignalProtocolAddress(localAddress.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID); + return new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).create(self, distributionId.asUuid()); + } + + /** + * Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients. + */ + public List sendSenderKeyDistributionMessage(List recipients, + List> unidentifiedAccess, + SenderKeyDistributionMessage message, + byte[] groupId) + throws IOException + { + ByteString distributionBytes = ByteString.copyFrom(message.serialize()); + Content content = Content.newBuilder().setSenderKeyDistributionMessage(distributionBytes).build(); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.of(groupId)); + long timestamp = System.currentTimeMillis(); + + return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null); + } + + /** + * Processes an inbound {@link SenderKeyDistributionMessage}. + */ + public void processSenderKeyDistributionMessage(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) { + new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).process(sender, senderKeyDistributionMessage); + } + + /** + * Sends a {@link SignalServiceDataMessage} to a group using sender keys. + */ + public List sendGroupDataMessage(DistributionId distributionId, + List recipients, + List unidentifiedAccess, + boolean isRecipientUpdate, + ContentHint contentHint, + SignalServiceDataMessage message) + throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException + { + Content content = createMessageContent(message); + Optional groupId = message.getGroupId(); + List results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId.orNull(), false); + + if (isMultiDevice.get()) { + Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.absent(), message.getTimestamp(), results, isRecipientUpdate); + EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null); + } + + return results; + } + + /** + * Sends a message to a group using client-side fanout. * * @param recipients The group members. * @param message The group message. * @throws IOException */ - public List sendMessage(List recipients, - List> unidentifiedAccess, - boolean isRecipientUpdate, - SignalServiceDataMessage message) + public List sendDataMessage(List recipients, + List> unidentifiedAccess, + boolean isRecipientUpdate, + ContentHint contentHint, + SignalServiceDataMessage message) throws IOException, UntrustedIdentityException { - byte[] content = createMessageContent(message); + Content content = createMessageContent(message); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null); + List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null); boolean needsSyncInResults = false; for (SendMessageResult result : results) { @@ -339,17 +460,19 @@ public class SignalServiceMessageSender { recipient = Optional.of(recipients.get(0)); } - byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate); - sendMessage(localAddress, Optional.absent(), timestamp, syncMessage, false, null); + Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate); + EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null); } return results; } - public void sendMessage(SignalServiceSyncMessage message, Optional unidentifiedAccess) + public void sendSyncMessage(SignalServiceSyncMessage message, Optional unidentifiedAccess) throws IOException, UntrustedIdentityException { - byte[] content; + Content content; if (message.getContacts().isPresent()) { content = createMultiDeviceContactsContent(message.getContacts().get().getContactsStream().asStream(), @@ -379,7 +502,7 @@ public class SignalServiceMessageSender { } else if (message.getKeys().isPresent()) { content = createMultiDeviceSyncKeysContent(message.getKeys().get()); } else if (message.getVerified().isPresent()) { - sendMessage(message.getVerified().get(), unidentifiedAccess); + sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess); return; } else { throw new IOException("Unsupported sync message!"); @@ -388,7 +511,9 @@ public class SignalServiceMessageSender { long timestamp = message.getSent().isPresent() ? message.getSent().get().getTimestamp() : System.currentTimeMillis(); - sendMessage(localAddress, Optional.absent(), timestamp, content, false, null); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(localAddress, Optional.absent(), timestamp, envelopeContent, false, null); } public void setSoTimeoutMillis(long soTimeoutMillis) { @@ -506,7 +631,7 @@ public class SignalServiceMessageSender { attachment.getUploadTimestamp()); } - private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) + private void sendVerifiedMessage(VerifiedMessage message, Optional unidentifiedAccess) throws IOException, UntrustedIdentityException { byte[] nullMessageBody = DataMessage.newBuilder() @@ -518,16 +643,19 @@ public class SignalServiceMessageSender { .setPadding(ByteString.copyFrom(nullMessageBody)) .build(); - byte[] content = Content.newBuilder() + Content content = Content.newBuilder() .setNullMessage(nullMessage) - .build() - .toByteArray(); + .build(); - SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, false, null); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); + + SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, false, null); if (result.getSuccess().isNeedsSync()) { - byte[] syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray()); - sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessage, false, null); + Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray()); + EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent()); + + sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null); } } @@ -543,15 +671,16 @@ public class SignalServiceMessageSender { .setPadding(ByteString.copyFrom(nullMessageBody)) .build(); - byte[] content = Content.newBuilder() - .setNullMessage(nullMessage) - .build() - .toByteArray(); + Content content = Content.newBuilder() + .setNullMessage(nullMessage) + .build(); - return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); + + return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null); } - private byte[] createTypingContent(SignalServiceTypingMessage message) { + private Content createTypingContent(SignalServiceTypingMessage message) { Content.Builder container = Content.newBuilder(); TypingMessage.Builder builder = TypingMessage.newBuilder(); @@ -565,10 +694,10 @@ public class SignalServiceMessageSender { builder.setGroupId(ByteString.copyFrom(message.getGroupId().get())); } - return container.setTypingMessage(builder).build().toByteArray(); + return container.setTypingMessage(builder).build(); } - private byte[] createReceiptContent(SignalServiceReceiptMessage message) { + private Content createReceiptContent(SignalServiceReceiptMessage message) { Content.Builder container = Content.newBuilder(); ReceiptMessage.Builder builder = ReceiptMessage.newBuilder(); @@ -580,10 +709,10 @@ public class SignalServiceMessageSender { else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ); else if (message.isViewedReceipt()) builder.setType(ReceiptMessage.Type.VIEWED); - return container.setReceiptMessage(builder).build().toByteArray(); + return container.setReceiptMessage(builder).build(); } - private byte[] createMessageContent(SignalServiceDataMessage message) throws IOException { + private Content createMessageContent(SignalServiceDataMessage message) throws IOException { Content.Builder container = Content.newBuilder(); DataMessage.Builder builder = DataMessage.newBuilder(); List pointers = createAttachmentPointers(message.getAttachments()); @@ -779,10 +908,10 @@ public class SignalServiceMessageSender { builder.setTimestamp(message.getTimestamp()); - return enforceMaxContentSize(container.setDataMessage(builder).build().toByteArray()); + return enforceMaxContentSize(container.setDataMessage(builder).build()); } - private byte[] createCallContent(SignalServiceCallMessage callMessage) { + private Content createCallContent(SignalServiceCallMessage callMessage) { Content.Builder container = Content.newBuilder(); CallMessage.Builder builder = CallMessage.newBuilder(); @@ -862,30 +991,30 @@ public class SignalServiceMessageSender { } container.setCallMessage(builder); - return container.build().toByteArray(); + return container.build(); } - private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException { + private Content createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException { Content.Builder container = Content.newBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder(); builder.setContacts(SyncMessage.Contacts.newBuilder() .setBlob(createAttachmentPointer(contacts)) .setComplete(complete)); - return container.setSyncMessage(builder).build().toByteArray(); + return container.setSyncMessage(builder).build(); } - private byte[] createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException { + private Content createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException { Content.Builder container = Content.newBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder(); builder.setGroups(SyncMessage.Groups.newBuilder() .setBlob(createAttachmentPointer(groups))); - return container.setSyncMessage(builder).build().toByteArray(); + return container.setSyncMessage(builder).build(); } - private byte[] createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional unidentifiedAccess) throws IOException { + private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional unidentifiedAccess) throws IOException { SignalServiceAddress address = transcript.getDestination().get(); SendMessageResult result = SendMessageResult.success(address, unidentifiedAccess.isPresent(), true, -1); @@ -896,60 +1025,56 @@ public class SignalServiceMessageSender { false); } - private byte[] createMultiDeviceSentTranscriptContent(byte[] content, Optional recipient, - long timestamp, List sendMessageResults, - boolean isRecipientUpdate) + private Content createMultiDeviceSentTranscriptContent(Content content, Optional recipient, + long timestamp, List sendMessageResults, + boolean isRecipientUpdate) { - try { - Content.Builder container = Content.newBuilder(); - SyncMessage.Builder syncMessage = createSyncMessageBuilder(); - SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder(); - DataMessage dataMessage = Content.parseFrom(content).getDataMessage(); + Content.Builder container = Content.newBuilder(); + SyncMessage.Builder syncMessage = createSyncMessageBuilder(); + SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder(); + DataMessage dataMessage = content.getDataMessage(); - sentMessage.setTimestamp(timestamp); - sentMessage.setMessage(dataMessage); + sentMessage.setTimestamp(timestamp); + sentMessage.setMessage(dataMessage); - for (SendMessageResult result : sendMessageResults) { - if (result.getSuccess() != null) { - SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder(); + for (SendMessageResult result : sendMessageResults) { + if (result.getSuccess() != null) { + SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder(); - if (result.getAddress().getUuid().isPresent()) { - builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString()); - } - - if (result.getAddress().getNumber().isPresent()) { - builder = builder.setDestinationE164(result.getAddress().getNumber().get()); - } - - builder.setUnidentified(result.getSuccess().isUnidentified()); - - sentMessage.addUnidentifiedStatus(builder.build()); + if (result.getAddress().getUuid().isPresent()) { + builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString()); } + + if (result.getAddress().getNumber().isPresent()) { + builder = builder.setDestinationE164(result.getAddress().getNumber().get()); + } + + builder.setUnidentified(result.getSuccess().isUnidentified()); + + sentMessage.addUnidentifiedStatus(builder.build()); } - - if (recipient.isPresent()) { - if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString()); - if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get()); - } - - if (dataMessage.getExpireTimer() > 0) { - sentMessage.setExpirationStartTimestamp(System.currentTimeMillis()); - } - - if (dataMessage.getIsViewOnce()) { - dataMessage = dataMessage.toBuilder().clearAttachments().build(); - sentMessage.setMessage(dataMessage); - } - - sentMessage.setIsRecipientUpdate(isRecipientUpdate); - - return container.setSyncMessage(syncMessage.setSent(sentMessage)).build().toByteArray(); - } catch (InvalidProtocolBufferException e) { - throw new AssertionError(e); } + + if (recipient.isPresent()) { + if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString()); + if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get()); + } + + if (dataMessage.getExpireTimer() > 0) { + sentMessage.setExpirationStartTimestamp(System.currentTimeMillis()); + } + + if (dataMessage.getIsViewOnce()) { + dataMessage = dataMessage.toBuilder().clearAttachments().build(); + sentMessage.setMessage(dataMessage); + } + + sentMessage.setIsRecipientUpdate(isRecipientUpdate); + + return container.setSyncMessage(syncMessage.setSent(sentMessage)).build(); } - private byte[] createMultiDeviceReadContent(List readMessages) { + private Content createMultiDeviceReadContent(List readMessages) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder(); @@ -967,10 +1092,10 @@ public class SignalServiceMessageSender { builder.addRead(readBuilder.build()); } - return container.setSyncMessage(builder).build().toByteArray(); + return container.setSyncMessage(builder).build(); } - private byte[] createMultiDeviceViewedContent(List readMessages) { + private Content createMultiDeviceViewedContent(List readMessages) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder(); @@ -988,10 +1113,10 @@ public class SignalServiceMessageSender { builder.addViewed(viewedBuilder.build()); } - return container.setSyncMessage(builder).build().toByteArray(); + return container.setSyncMessage(builder).build(); } - private byte[] createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) { + private Content createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder builder = createSyncMessageBuilder(); SyncMessage.ViewOnceOpen.Builder viewOnceBuilder = SyncMessage.ViewOnceOpen.newBuilder().setTimestamp(readMessage.getTimestamp()); @@ -1006,10 +1131,10 @@ public class SignalServiceMessageSender { builder.setViewOnceOpen(viewOnceBuilder.build()); - return container.setSyncMessage(builder).build().toByteArray(); + return container.setSyncMessage(builder).build(); } - private byte[] createMultiDeviceBlockedContent(BlockedListMessage blocked) { + private Content createMultiDeviceBlockedContent(BlockedListMessage blocked) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.Blocked.Builder blockedMessage = SyncMessage.Blocked.newBuilder(); @@ -1027,10 +1152,10 @@ public class SignalServiceMessageSender { blockedMessage.addGroupIds(ByteString.copyFrom(groupId)); } - return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build().toByteArray(); + return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build(); } - private byte[] createMultiDeviceConfigurationContent(ConfigurationMessage configuration) { + private Content createMultiDeviceConfigurationContent(ConfigurationMessage configuration) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.Configuration.Builder configurationMessage = SyncMessage.Configuration.newBuilder(); @@ -1053,10 +1178,10 @@ public class SignalServiceMessageSender { configurationMessage.setProvisioningVersion(ProvisioningProtos.ProvisioningVersion.CURRENT_VALUE); - return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build().toByteArray(); + return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build(); } - private byte[] createMultiDeviceStickerPackOperationContent(List stickerPackOperations) { + private Content createMultiDeviceStickerPackOperationContent(List stickerPackOperations) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); @@ -1081,10 +1206,10 @@ public class SignalServiceMessageSender { syncMessage.addStickerPackOperation(builder); } - return container.setSyncMessage(syncMessage).build().toByteArray(); + return container.setSyncMessage(syncMessage).build(); } - private byte[] createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) { + private Content createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.FetchLatest.Builder fetchMessage = SyncMessage.FetchLatest.newBuilder(); @@ -1101,10 +1226,10 @@ public class SignalServiceMessageSender { break; } - return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build().toByteArray(); + return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build(); } - private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) { + private Content createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder(); @@ -1143,10 +1268,10 @@ public class SignalServiceMessageSender { syncMessage.setMessageRequestResponse(responseMessage); - return container.setSyncMessage(syncMessage).build().toByteArray(); + return container.setSyncMessage(syncMessage).build(); } - private byte[] createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) { + private Content createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.OutgoingPayment.Builder paymentMessage = SyncMessage.OutgoingPayment.newBuilder(); @@ -1180,10 +1305,10 @@ public class SignalServiceMessageSender { syncMessage.setOutgoingPayment(paymentMessage); - return container.setSyncMessage(syncMessage).build().toByteArray(); + return container.setSyncMessage(syncMessage).build(); } - private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) { + private Content createMultiDeviceSyncKeysContent(KeysMessage keysMessage) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); SyncMessage.Keys.Builder builder = SyncMessage.Keys.newBuilder(); @@ -1194,10 +1319,10 @@ public class SignalServiceMessageSender { Log.w(TAG, "Invalid keys message!"); } - return container.setSyncMessage(syncMessage.setKeys(builder)).build().toByteArray(); + return container.setSyncMessage(syncMessage.setKeys(builder)).build(); } - private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) { + private Content createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) { Content.Builder container = Content.newBuilder(); SyncMessage.Builder syncMessage = createSyncMessageBuilder(); Verified.Builder verifiedMessageBuilder = Verified.newBuilder(); @@ -1221,7 +1346,7 @@ public class SignalServiceMessageSender { } syncMessage.setVerified(verifiedMessageBuilder); - return container.setSyncMessage(syncMessage).build().toByteArray(); + return container.setSyncMessage(syncMessage).build(); } private SyncMessage.Builder createSyncMessageBuilder() { @@ -1389,7 +1514,7 @@ public class SignalServiceMessageSender { private List sendMessage(List recipients, List> unidentifiedAccess, long timestamp, - byte[] content, + EnvelopeContent content, boolean online, CancelationSignal cancelationSignal) throws IOException @@ -1461,7 +1586,7 @@ public class SignalServiceMessageSender { private SendMessageResult sendMessage(SignalServiceAddress recipient, Optional unidentifiedAccess, long timestamp, - byte[] content, + EnvelopeContent content, boolean online, CancelationSignal cancelationSignal) throws UntrustedIdentityException, IOException @@ -1491,7 +1616,7 @@ public class SignalServiceMessageSender { return SendMessageResult.success(recipient, false, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime); } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) { Log.w(TAG, e); - Log.w(TAG, "[sendMessage] Pipe failed, falling back..."); + Log.w(TAG, "[sendMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); } } else if (unidentifiedPipe.isPresent() && unidentifiedAccess.isPresent()) { try { @@ -1530,7 +1655,190 @@ public class SignalServiceMessageSender { } } - throw new IOException("Failed to resolve conflicts after 3 attempts!"); + throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!"); + } + + /** + * Will send a message using sender keys to all of the specified recipients. It is assumed that + * all of the recipients have UUIDs. + * + * This method will handle sending out SenderKeyDistributionMessages as necessary. + */ + private List sendGroupMessage(DistributionId distributionId, + List recipients, + List unidentifiedAccess, + long timestamp, + Content content, + ContentHint contentHint, + byte[] groupId, + boolean online) + throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException + { + if (recipients.isEmpty()) { + Log.w(TAG, "[sendGroupMessage] Empty recipient list!"); + return Collections.emptyList(); + } + + Preconditions.checkArgument(recipients.size() == unidentifiedAccess.size(), "Unidentified access mismatch!"); + + if (recipients.stream().anyMatch(r -> !r.getUuid().isPresent())) { + throw new IllegalArgumentException("All recipients must have a UUID!"); + } + + Map accessByUuid = new HashMap<>(); + Iterator addressIterator = recipients.iterator(); + Iterator accessIterator = unidentifiedAccess.iterator(); + + while (addressIterator.hasNext()) { + accessByUuid.put(addressIterator.next().getUuid().get(), accessIterator.next()); + } + + for (int i = 0; i < RETRY_COUNT; i++) { + List destinations = new LinkedList<>(); + + for (SignalServiceAddress recipient : recipients) { + destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), SignalServiceAddress.DEFAULT_DEVICE_ID)); + + for (int deviceId : store.getSubDeviceSessions(recipient.getIdentifier())) { + if (store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) { + destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), deviceId)); + } + } + } + + Set sharedWith = store.getSenderKeySharedWith(distributionId); + List needsSenderKey = destinations.stream() + .filter(a -> !sharedWith.contains(a)) + .map(a -> UuidUtil.parseOrThrow(a.getName())) + .distinct() + .map(uuid -> new SignalServiceAddress(uuid, null)) + .collect(Collectors.toList()); + + if (needsSenderKey.size() > 0) { + Log.i(TAG, "[sendGroupMessage] Need to send the distribution message to " + needsSenderKey.size() + " addresses."); + SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId); + List> access = needsSenderKey.stream() + .map(r -> { + UnidentifiedAccess targetAccess = accessByUuid.get(r.getUuid().get()); + return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess)); + }) + .collect(Collectors.toList()); + + List results = sendSenderKeyDistributionMessage(needsSenderKey, access, message, groupId); + + List successes = results.stream() + .filter(SendMessageResult::isSuccess) + .map(SendMessageResult::getAddress) + .collect(Collectors.toList()); + + Set successUuids = successes.stream().map(a -> a.getUuid().get().toString()).collect(Collectors.toSet()); + Set successAddresses = destinations.stream().filter(a -> successUuids.contains(a.getName())).collect(Collectors.toSet());; + + store.markSenderKeySharedWith(distributionId, successAddresses); + + Log.i(TAG, "[sendGroupMessage] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients."); + + int failureCount = results.size() - successes.size(); + if (failureCount > 0) { + Log.w(TAG, "[sendGroupMessage] Failed to send sender keys to " + failureCount + " recipients. Sending back failed results now."); + + List trueFailures = results.stream() + .filter(r -> !r.isSuccess()) + .collect(Collectors.toList()); + + Set failedAddresses = trueFailures.stream() + .map(SendMessageResult::getAddress) + .collect(Collectors.toSet()); + + List fakeNetworkFailures = recipients.stream() + .filter(r -> !failedAddresses.contains(r)) + .map(SendMessageResult::networkFailure) + .collect(Collectors.toList()); + + List modifiedResults = new LinkedList<>(); + modifiedResults.addAll(trueFailures); + modifiedResults.addAll(fakeNetworkFailures); + + return modifiedResults; + } + } + + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sessionLock, null); + SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate(); + + byte[] ciphertext; + try { + ciphertext = cipher.encryptForGroup(distributionId, destinations, senderCertificate, content.toByteArray(), contentHint, groupId); + } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { + throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity()); + } + + byte[] joinedUnidentifiedAccess = new byte[16]; + for (UnidentifiedAccess access : unidentifiedAccess) { + joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey()); + } + + Optional pipe = this.unidentifiedPipe.get(); + + if (pipe.isPresent()) { + try { + SendGroupMessageResponse response = pipe.get().sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online).get(10, TimeUnit.SECONDS); + return transformGroupResponseToMessageResults(recipients, response); + } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) { + Log.w(TAG, "[sendGroupMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")"); + } + } else { + Log.d(TAG, "[sendGroupMessage] No pipe available."); + } + + try { + SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online); + return transformGroupResponseToMessageResults(recipients, response); + } catch (GroupMismatchedDevicesException e) { + Log.w(TAG, "[sendGroupMessage] Handling mismatched devices.", e); + for (GroupMismatchedDevices mismatched : e.getMismatchedDevices()) { + SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(mismatched.getUuid()), Optional.absent()); + handleMismatchedDevices(socket, address, mismatched.getDevices()); + List clearAddresses = mismatched.getDevices().getExtraDevices().stream() + .map(device -> new SignalProtocolAddress(address.getIdentifier(), device)) + .collect(Collectors.toList()); + store.clearSenderKeySharedWith(distributionId, clearAddresses); + } + } catch (GroupStaleDevicesException e) { + Log.w(TAG, "[sendGroupMessage] Handling stale devices.", e); + for (GroupStaleDevices stale : e.getStaleDevices()) { + SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(stale.getUuid()), Optional.absent()); + handleStaleDevices(address, stale.getDevices()); + List clearAddresses = stale.getDevices().getStaleDevices().stream() + .map(device -> new SignalProtocolAddress(address.getIdentifier(), device)) + .collect(Collectors.toList()); + store.clearSenderKeySharedWith(distributionId, clearAddresses); + } + } + + Log.w(TAG, "[sendGroupMessage] Attempt failed (i = " + i + ")"); + } + + throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!"); + } + + private List transformGroupResponseToMessageResults(List recipients, SendGroupMessageResponse response) { + Set unregistered = response.getUnsentTargets(); + + List failures = unregistered.stream() + .map(uuid -> new SignalServiceAddress(uuid, null)) + .map(SendMessageResult::unregisteredFailure) + .collect(Collectors.toList()); + + List success = recipients.stream() + .filter(r -> !unregistered.contains(r.getUuid().get())) + .map(a -> SendMessageResult.success(a, true, isMultiDevice.get(), -1)) + .collect(Collectors.toList()); + + List results = new LinkedList<>(success); + results.addAll(failures); + + return results; } private List createAttachmentPointers(Optional> attachments) throws IOException { @@ -1625,7 +1933,7 @@ public class SignalServiceMessageSender { SignalServiceAddress recipient, Optional unidentifiedAccess, long timestamp, - byte[] plaintext, + EnvelopeContent plaintext, boolean online) throws IOException, InvalidKeyException, UntrustedIdentityException { @@ -1648,7 +1956,7 @@ public class SignalServiceMessageSender { SignalServiceAddress recipient, Optional unidentifiedAccess, int deviceId, - byte[] plaintext) + EnvelopeContent plaintext) throws IOException, InvalidKeyException, UntrustedIdentityException { SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId); @@ -1742,9 +2050,20 @@ public class SignalServiceMessageSender { return results; } - private byte[] enforceMaxContentSize(byte[] content) { - if (maxEnvelopeSize > 0 && content.length > maxEnvelopeSize) { - throw new ContentTooLargeException(content.length); + private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) { + int size = content.size(); + + if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) { + throw new ContentTooLargeException(size); + } + return content; + } + + private Content enforceMaxContentSize(Content content) { + int size = content.toByteArray().length; + + if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) { + throw new ContentTooLargeException(size); } return content; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceProtocolStore.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceProtocolStore.java index 24b3578d18..7f0a4c6484 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceProtocolStore.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceProtocolStore.java @@ -6,5 +6,5 @@ import org.whispersystems.libsignal.state.SignalProtocolStore; * And extension of the normal protocol store interface that has additional methods that are needed * in the service layer, but not the protocol layer. */ -public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore { +public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore, SignalServiceSenderKeyStore { } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceSenderKeyStore.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceSenderKeyStore.java new file mode 100644 index 0000000000..49dd327f2c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceSenderKeyStore.java @@ -0,0 +1,29 @@ +package org.whispersystems.signalservice.api; + +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyStore; +import org.whispersystems.signalservice.api.push.DistributionId; + +import java.util.Collection; +import java.util.Set; + +/** + * And extension of the normal protocol sender key store interface that has additional methods that are + * needed in the service layer, but not the protocol layer. + */ +public interface SignalServiceSenderKeyStore extends SenderKeyStore { + /** + * @return A set of protocol addresses that have previously been sent the sender key data for the provided distributionId. + */ + Set getSenderKeySharedWith(DistributionId distributionId); + + /** + * Marks the provided addresses as having been sent the sender key data for the provided distributionId. + */ + void markSenderKeySharedWith(DistributionId distributionId, Collection addresses); + + /** + * Marks the provided addresses as not knowing about the provided distributionId. + */ + void clearSenderKeySharedWith(DistributionId distributionId, Collection addresses); +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java index be9342fe2e..f4368db609 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java @@ -128,14 +128,18 @@ public class AccountAttributes { @JsonProperty("gv1-migration") private boolean gv1Migration; + @JsonProperty + private boolean senderKey; + @JsonCreator public Capabilities() {} - public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration) { + public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey) { this.uuid = uuid; this.gv2 = gv2; this.storage = storage; this.gv1Migration = gv1Migration; + this.senderKey = senderKey; } public boolean isUuid() { @@ -153,5 +157,9 @@ public class AccountAttributes { public boolean isGv1Migration() { return gv1Migration; } + + public boolean isSenderKey() { + return senderKey; + } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ContentHint.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ContentHint.java new file mode 100644 index 0000000000..4f7b4082f8 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/ContentHint.java @@ -0,0 +1,38 @@ +package org.whispersystems.signalservice.api.crypto; + +import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent; + +import java.util.HashMap; +import java.util.Map; + +public enum ContentHint { + /** This message has content, but you shouldn’t expect it to be re-sent to you. */ + DEFAULT(UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT), + + /** You should expect to be able to have this content be re-sent to you. */ + RESENDABLE(UnidentifiedSenderMessageContent.CONTENT_HINT_RESENDABLE), + + /** This message has no real content and likely cannot be re-sent to you. */ + IMPLICIT(UnidentifiedSenderMessageContent.CONTENT_HINT_IMPLICIT); + + private static final Map TYPE_MAP = new HashMap<>(); + static { + for (ContentHint value : values()) { + TYPE_MAP.put(value.getType(), value); + } + } + + private final int type; + + ContentHint(int type) { + this.type = type; + } + + public int getType() { + return type; + } + + public static ContentHint fromType(int type) { + return TYPE_MAP.getOrDefault(type, DEFAULT); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeContent.java new file mode 100644 index 0000000000..5166f98b88 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/EnvelopeContent.java @@ -0,0 +1,156 @@ +package org.whispersystems.signalservice.api.crypto; + +import org.signal.libsignal.metadata.certificate.SenderCertificate; +import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.UntrustedIdentityException; +import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.protocol.PlaintextContent; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; +import org.whispersystems.signalservice.internal.push.PushTransportDetails; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type; +import org.whispersystems.util.Base64; + +/** + * An abstraction over the different types of message contents we can have. + */ +public interface EnvelopeContent { + + /** + * Processes the content using sealed sender. + */ + OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher, + SignalSealedSessionCipher sealedSessionCipher, + SignalProtocolAddress destination, + SenderCertificate senderCertificate) + throws UntrustedIdentityException, InvalidKeyException; + + /** + * Processes the content using unsealed sender. + */ + OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException; + + /** + * An estimated size, in bytes. + */ + int size(); + + /** + * Wrap {@link Content} you plan on sending as an encrypted message. + * This is the default. Consider anything else exceptional. + */ + static EnvelopeContent encrypted(Content content, ContentHint contentHint, Optional groupId) { + return new Encrypted(content.toByteArray(), contentHint, groupId); + } + + /** + * Wraps a {@link PlaintextContent}. This is exceptional, currently limited only to {@link DecryptionErrorMessage}. + */ + static EnvelopeContent plaintext(PlaintextContent content, Optional groupId) { + return new Plaintext(content, groupId); + } + + class Encrypted implements EnvelopeContent { + + private final byte[] unpaddedMessage; + private final ContentHint contentHint; + private final Optional groupId; + + public Encrypted(byte[] unpaddedMessage, ContentHint contentHint, Optional groupId) { + this.unpaddedMessage = unpaddedMessage; + this.contentHint = contentHint; + this.groupId = groupId; + } + + @Override + public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher, + SignalSealedSessionCipher sealedSessionCipher, + SignalProtocolAddress destination, + SenderCertificate senderCertificate) + throws UntrustedIdentityException, InvalidKeyException + { + PushTransportDetails transportDetails = new PushTransportDetails(); + CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); + UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message, + senderCertificate, + contentHint.getType(), + groupId); + + byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent); + String body = Base64.encodeBytes(ciphertext); + int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination); + + return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body); + } + + @Override + public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException { + PushTransportDetails transportDetails = new PushTransportDetails(); + CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); + int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(); + String body = Base64.encodeBytes(message.serialize()); + + int type; + + switch (message.getType()) { + case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break; + case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break; + default: throw new AssertionError("Bad type: " + message.getType()); + } + + return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body); + } + + @Override + public int size() { + return unpaddedMessage.length; + } + } + + class Plaintext implements EnvelopeContent { + + private final PlaintextContent plaintextContent; + private final Optional groupId; + + public Plaintext(PlaintextContent plaintextContent, Optional groupId) { + this.plaintextContent = plaintextContent; + this.groupId = groupId; + } + + @Override + public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher, + SignalSealedSessionCipher sealedSessionCipher, + SignalProtocolAddress destination, + SenderCertificate senderCertificate) + throws UntrustedIdentityException, InvalidKeyException + { + UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(plaintextContent, + senderCertificate, + ContentHint.IMPLICIT.getType(), + groupId); + + byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent); + String body = Base64.encodeBytes(ciphertext); + int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination); + + return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body); + } + + @Override + public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) { + String body = Base64.encodeBytes(plaintextContent.getBody()); + int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(); + + return new OutgoingPushMessage(Type.PLAINTEXT_CONTENT_VALUE, destination.getDeviceId(), remoteRegistrationId, body); + } + + @Override + public int size() { + return plaintextContent.getBody().length; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupCipher.java new file mode 100644 index 0000000000..0ff7ebfe4b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupCipher.java @@ -0,0 +1,39 @@ +package org.whispersystems.signalservice.api.crypto; + +import org.whispersystems.libsignal.DuplicateMessageException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.LegacyMessageException; +import org.whispersystems.libsignal.NoSessionException; +import org.whispersystems.libsignal.groups.GroupCipher; +import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.util.UUID; + +/** + * A thread-safe wrapper around {@link GroupCipher}. + */ +public class SignalGroupCipher { + + private final SignalSessionLock lock; + private final GroupCipher cipher; + + public SignalGroupCipher(SignalSessionLock lock, GroupCipher cipher) { + this.lock = lock; + this.cipher = cipher; + } + + public CiphertextMessage encrypt(UUID distributionId, byte[] paddedPlaintext) throws NoSessionException { + try (SignalSessionLock.Lock unused = lock.acquire()) { + return cipher.encrypt(distributionId, paddedPlaintext); + } + } + + public byte[] decrypt(byte[] senderKeyMessageBytes) + throws LegacyMessageException, DuplicateMessageException, InvalidMessageException, NoSessionException + { + try (SignalSessionLock.Lock unused = lock.acquire()) { + return cipher.decrypt(senderKeyMessageBytes); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupSessionBuilder.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupSessionBuilder.java new file mode 100644 index 0000000000..4cfc783758 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalGroupSessionBuilder.java @@ -0,0 +1,35 @@ +package org.whispersystems.signalservice.api.crypto; + +import org.whispersystems.libsignal.SessionBuilder; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.GroupSessionBuilder; +import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage; +import org.whispersystems.signalservice.api.SignalSessionLock; + +import java.util.UUID; + +/** + * A thread-safe wrapper around {@link SessionBuilder}. + */ +public class SignalGroupSessionBuilder { + + private final SignalSessionLock lock; + private final GroupSessionBuilder builder; + + public SignalGroupSessionBuilder(SignalSessionLock lock, GroupSessionBuilder builder) { + this.lock = lock; + this.builder = builder; + } + + public void process(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) { + try (SignalSessionLock.Lock unused = lock.acquire()) { + builder.process(sender, senderKeyDistributionMessage); + } + } + + public SenderKeyDistributionMessage create(SignalProtocolAddress sender, UUID distributionId) { + try (SignalSessionLock.Lock unused = lock.acquire()) { + return builder.create(sender, distributionId); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalSealedSessionCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalSealedSessionCipher.java index 89361647b3..3195c117f6 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalSealedSessionCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalSealedSessionCipher.java @@ -14,10 +14,15 @@ import org.signal.libsignal.metadata.SealedSessionCipher; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.libsignal.metadata.certificate.SenderCertificate; +import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.UntrustedIdentityException; import org.whispersystems.signalservice.api.SignalSessionLock; +import java.util.List; + /** * A thread-safe wrapper around {@link SealedSessionCipher}. */ @@ -31,9 +36,19 @@ public class SignalSealedSessionCipher { this.cipher = cipher; } - public byte[] encrypt(SignalProtocolAddress destinationAddress, SenderCertificate senderCertificate, byte[] paddedPlaintext) throws InvalidKeyException, org.whispersystems.libsignal.UntrustedIdentityException { + public byte[] encrypt(SignalProtocolAddress destinationAddress, UnidentifiedSenderMessageContent content) + throws InvalidKeyException, UntrustedIdentityException + { try (SignalSessionLock.Lock unused = lock.acquire()) { - return cipher.encrypt(destinationAddress, senderCertificate, paddedPlaintext); + return cipher.encrypt(destinationAddress, content); + } + } + + public byte[] multiRecipientEncrypt(List recipients, UnidentifiedSenderMessageContent content) + throws InvalidKeyException, UntrustedIdentityException, NoSessionException + { + try (SignalSessionLock.Lock unused = lock.acquire()) { + return cipher.multiRecipientEncrypt(recipients, content); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index 53f63068df..28bc72da1a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -22,6 +22,8 @@ import org.signal.libsignal.metadata.SealedSessionCipher; import org.signal.libsignal.metadata.SealedSessionCipher.DecryptionResult; import org.signal.libsignal.metadata.SelfSendException; import org.signal.libsignal.metadata.certificate.CertificateValidator; +import org.signal.libsignal.metadata.certificate.SenderCertificate; +import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent; import org.whispersystems.libsignal.DuplicateMessageException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidKeyIdException; @@ -32,7 +34,9 @@ import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.SessionCipher; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.UntrustedIdentityException; +import org.whispersystems.libsignal.groups.GroupCipher; import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.libsignal.protocol.PlaintextContent; import org.whispersystems.libsignal.protocol.PreKeySignalMessage; import org.whispersystems.libsignal.protocol.SignalMessage; import org.whispersystems.libsignal.state.SignalProtocolStore; @@ -41,6 +45,7 @@ import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceMetadata; +import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; @@ -53,10 +58,10 @@ import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadata import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto; import org.whispersystems.util.Base64; +import java.util.List; + /** - * This is used to decrypt received {@link SignalServiceEnvelope}s. - * - * @author Moxie Marlinspike + * This is used to encrypt + decrypt received {@link SignalServiceEnvelope}s. */ public class SignalServiceCipher { @@ -79,35 +84,41 @@ public class SignalServiceCipher { this.certificateValidator = certificateValidator; } + public byte[] encryptForGroup(DistributionId distributionId, + List destinations, + SenderCertificate senderCertificate, + byte[] unpaddedMessage, + ContentHint contentHint, + byte[] groupId) + throws NoSessionException, UntrustedIdentityException, InvalidKeyException + { + PushTransportDetails transport = new PushTransportDetails(); + SignalProtocolAddress localProtocolAddress = new SignalProtocolAddress(localAddress.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID); + SignalGroupCipher groupCipher = new SignalGroupCipher(sessionLock, new GroupCipher(signalProtocolStore, localProtocolAddress)); + SignalSealedSessionCipher sessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1)); + CiphertextMessage message = groupCipher.encrypt(distributionId.asUuid(), transport.getPaddedMessageBody(unpaddedMessage)); + UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message, + senderCertificate, + contentHint.getType(), + Optional.of(groupId)); + + return sessionCipher.multiRecipientEncrypt(destinations, messageContent); + } + public OutgoingPushMessage encrypt(SignalProtocolAddress destination, Optional unidentifiedAccess, - byte[] unpaddedMessage) + EnvelopeContent content) throws UntrustedIdentityException, InvalidKeyException { if (unidentifiedAccess.isPresent()) { - SignalSealedSessionCipher sessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1)); - PushTransportDetails transportDetails = new PushTransportDetails(); - byte[] ciphertext = sessionCipher.encrypt(destination, unidentifiedAccess.get().getUnidentifiedCertificate(), transportDetails.getPaddedMessageBody(unpaddedMessage)); - String body = Base64.encodeBytes(ciphertext); - int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(destination); + SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination)); + SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1)); - return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body); + return content.processSealedSender(sessionCipher, sealedSessionCipher, destination, unidentifiedAccess.get().getUnidentifiedCertificate()); } else { - SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination)); - PushTransportDetails transportDetails = new PushTransportDetails(); - CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); - int remoteRegistrationId = sessionCipher.getRemoteRegistrationId(); - String body = Base64.encodeBytes(message.serialize()); + SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, destination)); - int type; - - switch (message.getType()) { - case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break; - case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break; - default: throw new AssertionError("Bad type: " + message.getType()); - } - - return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body); + return content.processUnsealedSender(sessionCipher, destination); } } @@ -179,20 +190,21 @@ public class SignalServiceCipher { SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress)); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid()); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.absent()); } else if (envelope.isSignalMessage()) { SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice()); SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress)); paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid()); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid(), Optional.absent()); } else if (envelope.isUnidentifiedSender()) { - SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1)); + SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), SignalServiceAddress.DEFAULT_DEVICE_ID)); DecryptionResult result = sealedSessionCipher.decrypt(certificateValidator, ciphertext, envelope.getServerReceivedTimestamp()); SignalServiceAddress resultAddress = new SignalServiceAddress(UuidUtil.parse(result.getSenderUuid()), result.getSenderE164()); + Optional groupId = result.getGroupId(); paddedMessage = result.getPaddedMessage(); - metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid()); + metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid(), groupId); } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java index 35212cfc78..5e8a626f73 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java @@ -42,6 +42,10 @@ public class SendMessageResult { return success; } + public boolean isSuccess() { + return success != null; + } + public boolean isNetworkFailure() { return networkFailure || proofRequiredFailure != null; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 5882dc47ac..b900ee692e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -16,7 +16,10 @@ import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.LegacyMessageException; import org.whispersystems.libsignal.logging.Log; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.BusyMessage; @@ -70,14 +73,18 @@ public final class SignalServiceContent { private final boolean needsReceipt; private final SignalServiceContentProto serializedState; private final String serverUuid; + private final Optional groupId; - private final Optional message; - private final Optional synchronizeMessage; - private final Optional callMessage; - private final Optional readMessage; - private final Optional typingMessage; + private final Optional message; + private final Optional synchronizeMessage; + private final Optional callMessage; + private final Optional readMessage; + private final Optional typingMessage; + private final Optional senderKeyDistributionMessage; + private final Optional decryptionErrorMessage; private SignalServiceContent(SignalServiceDataMessage message, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -85,6 +92,7 @@ public final class SignalServiceContent { long serverDeliveredTimestamp, boolean needsReceipt, String serverUuid, + Optional groupId, SignalServiceContentProto serializedState) { this.sender = sender; @@ -94,16 +102,20 @@ public final class SignalServiceContent { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; + this.groupId = groupId; this.serializedState = serializedState; - this.message = Optional.fromNullable(message); - this.synchronizeMessage = Optional.absent(); - this.callMessage = Optional.absent(); - this.readMessage = Optional.absent(); - this.typingMessage = Optional.absent(); + this.message = Optional.fromNullable(message); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.absent(); } private SignalServiceContent(SignalServiceSyncMessage synchronizeMessage, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -111,6 +123,7 @@ public final class SignalServiceContent { long serverDeliveredTimestamp, boolean needsReceipt, String serverUuid, + Optional groupId, SignalServiceContentProto serializedState) { this.sender = sender; @@ -120,16 +133,20 @@ public final class SignalServiceContent { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; + this.groupId = groupId; this.serializedState = serializedState; - this.message = Optional.absent(); - this.synchronizeMessage = Optional.fromNullable(synchronizeMessage); - this.callMessage = Optional.absent(); - this.readMessage = Optional.absent(); - this.typingMessage = Optional.absent(); + this.message = Optional.absent(); + this.synchronizeMessage = Optional.fromNullable(synchronizeMessage); + this.callMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.absent(); } private SignalServiceContent(SignalServiceCallMessage callMessage, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -137,6 +154,7 @@ public final class SignalServiceContent { long serverDeliveredTimestamp, boolean needsReceipt, String serverUuid, + Optional groupId, SignalServiceContentProto serializedState) { this.sender = sender; @@ -146,16 +164,20 @@ public final class SignalServiceContent { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; + this.groupId = groupId; this.serializedState = serializedState; - this.message = Optional.absent(); - this.synchronizeMessage = Optional.absent(); - this.callMessage = Optional.of(callMessage); - this.readMessage = Optional.absent(); - this.typingMessage = Optional.absent(); + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.of(callMessage); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.absent(); } private SignalServiceContent(SignalServiceReceiptMessage receiptMessage, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -163,6 +185,7 @@ public final class SignalServiceContent { long serverDeliveredTimestamp, boolean needsReceipt, String serverUuid, + Optional groupId, SignalServiceContentProto serializedState) { this.sender = sender; @@ -172,16 +195,51 @@ public final class SignalServiceContent { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; + this.groupId = groupId; this.serializedState = serializedState; - this.message = Optional.absent(); - this.synchronizeMessage = Optional.absent(); - this.callMessage = Optional.absent(); - this.readMessage = Optional.of(receiptMessage); - this.typingMessage = Optional.absent(); + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.readMessage = Optional.of(receiptMessage); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.absent(); + } + + private SignalServiceContent(DecryptionErrorMessage errorMessage, + Optional senderKeyDistributionMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + String serverUuid, + Optional groupId, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; + this.groupId = groupId; + this.serializedState = serializedState; + + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.of(errorMessage); } private SignalServiceContent(SignalServiceTypingMessage typingMessage, + Optional senderKeyDistributionMessage, SignalServiceAddress sender, int senderDevice, long timestamp, @@ -189,6 +247,7 @@ public final class SignalServiceContent { long serverDeliveredTimestamp, boolean needsReceipt, String serverUuid, + Optional groupId, SignalServiceContentProto serializedState) { this.sender = sender; @@ -198,13 +257,46 @@ public final class SignalServiceContent { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverUuid = serverUuid; + this.groupId = groupId; this.serializedState = serializedState; - this.message = Optional.absent(); - this.synchronizeMessage = Optional.absent(); - this.callMessage = Optional.absent(); - this.readMessage = Optional.absent(); - this.typingMessage = Optional.of(typingMessage); + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.of(typingMessage); + this.senderKeyDistributionMessage = senderKeyDistributionMessage; + this.decryptionErrorMessage = Optional.absent(); + } + + private SignalServiceContent(SenderKeyDistributionMessage senderKeyDistributionMessage, + SignalServiceAddress sender, + int senderDevice, + long timestamp, + long serverReceivedTimestamp, + long serverDeliveredTimestamp, + boolean needsReceipt, + String serverUuid, + Optional groupId, + SignalServiceContentProto serializedState) + { + this.sender = sender; + this.senderDevice = senderDevice; + this.timestamp = timestamp; + this.serverReceivedTimestamp = serverReceivedTimestamp; + this.serverDeliveredTimestamp = serverDeliveredTimestamp; + this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; + this.groupId = groupId; + this.serializedState = serializedState; + + this.message = Optional.absent(); + this.synchronizeMessage = Optional.absent(); + this.callMessage = Optional.absent(); + this.readMessage = Optional.absent(); + this.typingMessage = Optional.absent(); + this.senderKeyDistributionMessage = Optional.of(senderKeyDistributionMessage); + this.decryptionErrorMessage = Optional.absent(); } public Optional getDataMessage() { @@ -227,6 +319,14 @@ public final class SignalServiceContent { return typingMessage; } + public Optional getSenderKeyDistributionMessage() { + return senderKeyDistributionMessage; + } + + public Optional getDecryptionErrorMessage() { + return decryptionErrorMessage; + } + public SignalServiceAddress getSender() { return sender; } @@ -255,6 +355,10 @@ public final class SignalServiceContent { return serverUuid; } + public Optional getGroupId() { + return groupId; + } + public byte[] serialize() { return serializedState.toByteArray(); } @@ -285,6 +389,7 @@ public final class SignalServiceContent { SignalServiceProtos.DataMessage message = serviceContentProto.getLegacyDataMessage(); return new SignalServiceContent(createSignalServiceMessage(metadata, message), + Optional.absent(), metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -292,12 +397,23 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { - SignalServiceProtos.Content message = serviceContentProto.getContent(); + SignalServiceProtos.Content message = serviceContentProto.getContent(); + Optional senderKeyDistributionMessage = Optional.absent(); + + if (message.hasSenderKeyDistributionMessage()) { + try { + senderKeyDistributionMessage = Optional.of(new SenderKeyDistributionMessage(message.getSenderKeyDistributionMessage().toByteArray())); + } catch (LegacyMessageException | InvalidMessageException e) { + Log.w(TAG, "Failed to parse SenderKeyDistributionMessage!", e); + } + } if (message.hasDataMessage()) { return new SignalServiceContent(createSignalServiceMessage(metadata, message.getDataMessage()), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -305,9 +421,11 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -315,9 +433,11 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } else if (message.hasCallMessage()) { return new SignalServiceContent(createCallMessage(message.getCallMessage()), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -325,9 +445,11 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } else if (message.hasReceiptMessage()) { return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -335,9 +457,11 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } else if (message.hasTypingMessage()) { return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()), + senderKeyDistributionMessage, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp(), @@ -345,6 +469,30 @@ public final class SignalServiceContent { metadata.getServerDeliveredTimestamp(), false, metadata.getServerGuid(), + metadata.getGroupId(), + serviceContentProto); + } else if (message.hasDecryptionErrorMessage()) { + return new SignalServiceContent(createDecryptionErrorMessage(metadata, message.getDecryptionErrorMessage()), + senderKeyDistributionMessage, + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), + metadata.isNeedsReceipt(), + metadata.getServerGuid(), + metadata.getGroupId(), + serviceContentProto); + } else if (senderKeyDistributionMessage.isPresent()) { + return new SignalServiceContent(senderKeyDistributionMessage.get(), + metadata.getSender(), + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), + false, + metadata.getServerGuid(), + metadata.getGroupId(), serviceContentProto); } } @@ -720,6 +868,14 @@ public final class SignalServiceContent { return new SignalServiceReceiptMessage(type, content.getTimestampList(), metadata.getTimestamp()); } + private static DecryptionErrorMessage createDecryptionErrorMessage(SignalServiceMetadata metadata, ByteString content) throws ProtocolInvalidMessageException { + try { + return new DecryptionErrorMessage(content.toByteArray()); + } catch (InvalidMessageException e) { + throw new ProtocolInvalidMessageException(e, metadata.getSender().getIdentifier(), metadata.getSenderDevice()); + } + } + private static SignalServiceTypingMessage createTypingMessage(SignalServiceMetadata metadata, SignalServiceProtos.TypingMessage content) throws ProtocolInvalidMessageException { SignalServiceTypingMessage.Action action; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index 6a1cdcc5cc..812d0a7530 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -6,6 +6,7 @@ package org.whispersystems.signalservice.api.messages; +import org.signal.zkgroup.groups.GroupSecretParams; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.shared.SharedContact; @@ -234,6 +235,20 @@ public class SignalServiceDataMessage { return payment; } + public Optional getGroupId() { + byte[] groupId = null; + + if (getGroupContext().isPresent() && getGroupContext().get().getGroupV2().isPresent()) { + SignalServiceGroupV2 gv2 = getGroupContext().get().getGroupV2().get(); + groupId = GroupSecretParams.deriveFromMasterKey(gv2.getMasterKey()) + .getPublicParams() + .getGroupIdentifier() + .serialize(); + } + + return Optional.fromNullable(groupId); + } + public static class Builder { private List attachments = new LinkedList<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index b068efc1dc..a5a6e05743 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -246,6 +246,10 @@ public class SignalServiceEnvelope { return envelope.getType().getNumber() == Envelope.Type.UNIDENTIFIED_SENDER_VALUE; } + public boolean isPlaintextContent() { + return envelope.getType().getNumber() == Envelope.Type.PLAINTEXT_CONTENT_VALUE; + } + public byte[] serialize() { SignalServiceEnvelopeProto.Builder builder = SignalServiceEnvelopeProto.newBuilder() .setType(getType()) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java index f4f500d890..e308ebce6d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java @@ -1,5 +1,6 @@ package org.whispersystems.signalservice.api.messages; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; public final class SignalServiceMetadata { @@ -10,6 +11,7 @@ public final class SignalServiceMetadata { private final long serverDeliveredTimestamp; private final boolean needsReceipt; private final String serverGuid; + private final Optional groupId; public SignalServiceMetadata(SignalServiceAddress sender, int senderDevice, @@ -17,7 +19,8 @@ public final class SignalServiceMetadata { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, - String serverGuid) + String serverGuid, + Optional groupId) { this.sender = sender; this.senderDevice = senderDevice; @@ -26,6 +29,7 @@ public final class SignalServiceMetadata { this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; this.serverGuid = serverGuid; + this.groupId = groupId; } public SignalServiceAddress getSender() { @@ -55,4 +59,8 @@ public final class SignalServiceMetadata { public String getServerGuid() { return serverGuid; } + + public Optional getGroupId() { + return groupId; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index cc677079de..801bb7ab92 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -121,6 +121,8 @@ public class SignalServiceProfile { @JsonProperty("gv1-migration") private boolean gv1Migration; + private boolean senderKey; + @JsonCreator public Capabilities() {} @@ -135,6 +137,10 @@ public class SignalServiceProfile { public boolean isGv1Migration() { return gv1Migration; } + + public boolean isSenderKey() { + return senderKey; + } } public ProfileKeyCredentialResponse getProfileKeyCredentialResponse() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/DistributionId.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/DistributionId.java new file mode 100644 index 0000000000..b369f374b4 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/DistributionId.java @@ -0,0 +1,41 @@ +package org.whispersystems.signalservice.api.push; + +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.UUID; + +/** + * Represents the distributionId that is used to identify this group's sender key session. + * + * This is just a UUID, but we wrap it in order to provide some type safety and limit confusion + * around the multiple UUIDs we throw around. + */ +public final class DistributionId { + + private final UUID uuid; + + public static DistributionId from(String id) { + return new DistributionId(UuidUtil.parseOrThrow(id)); + } + + public static DistributionId from(UUID uuid) { + return new DistributionId(uuid); + } + + public static DistributionId create() { + return new DistributionId(UUID.randomUUID()); + } + + private DistributionId(UUID uuid) { + this.uuid = uuid; + } + + public UUID asUuid() { + return uuid; + } + + @Override + public String toString() { + return uuid.toString(); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java index d69424cc5e..bb0d406c05 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/NonSuccessfulResponseCodeException.java @@ -16,11 +16,12 @@ public class NonSuccessfulResponseCodeException extends IOException { private final int code; public NonSuccessfulResponseCodeException(int code) { + super("StatusCode: " + code); this.code = code; } public NonSuccessfulResponseCodeException(int code, String s) { - super(s); + super("[" + code + "] " + s); this.code = code; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevices.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevices.java new file mode 100644 index 0000000000..8c3f0712d2 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevices.java @@ -0,0 +1,24 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the body of a 409 response from the service during a sender key send. + */ +public class GroupMismatchedDevices { + @JsonProperty + private String uuid; + + @JsonProperty + private MismatchedDevices devices; + + public GroupMismatchedDevices() {} + + public String getUuid() { + return uuid; + } + + public MismatchedDevices getDevices() { + return devices; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupStaleDevices.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupStaleDevices.java new file mode 100644 index 0000000000..6ceaf281f9 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/GroupStaleDevices.java @@ -0,0 +1,23 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the body of a 410 response from the service during a sender key send. + */ +public class GroupStaleDevices { + + @JsonProperty + private String uuid; + + @JsonProperty + private StaleDevices devices; + + public String getUuid() { + return uuid; + } + + public StaleDevices getDevices() { + return devices; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index af685ed885..a62f465c04 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -86,8 +86,11 @@ import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResp import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException; import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException; +import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException; import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException; +import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException; +import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException; import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException; @@ -189,6 +192,7 @@ public class PushServiceSocket { private static final String DIRECTORY_AUTH_PATH = "/v1/directory/auth"; private static final String DIRECTORY_FEEDBACK_PATH = "/v1/directory/feedback-v3/%s"; private static final String MESSAGE_PATH = "/v1/messages/%s"; + private static final String GROUP_MESSAGE_PATH = "/v1/messages/multi_recipient?ts=%s&online=%s"; private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d"; private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s"; private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload"; @@ -407,6 +411,64 @@ public class PushServiceSocket { return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); } + public SendGroupMessageResponse sendGroupMessage(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online) + throws IOException + { + ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random); + + String path = String.format(Locale.US, GROUP_MESSAGE_PATH, timestamp, online); + + Request.Builder requestBuilder = new Request.Builder(); + requestBuilder.url(String.format("%s%s", connectionHolder.getUrl(), path)); + requestBuilder.put(RequestBody.create(MediaType.get("application/vnd.signal-messenger.mrm"), body)); + requestBuilder.addHeader("Unidentified-Access-Key", Base64.encodeBytes(joinedUnidentifiedAccess)); + + if (signalAgent != null) { + requestBuilder.addHeader("X-Signal-Agent", signalAgent); + } + + if (connectionHolder.getHostHeader().isPresent()) { + requestBuilder.addHeader("Host", connectionHolder.getHostHeader().get()); + } + + Call call = connectionHolder.getUnidentifiedClient().newCall(requestBuilder.build()); + + synchronized (connections) { + connections.add(call); + } + + Response response; + + try { + response = call.execute(); + } catch (IOException e) { + throw new PushNetworkException(e); + } finally { + synchronized (connections) { + connections.remove(call); + } + } + + switch (response.code()) { + case 200: + return readBodyJson(response.body(), SendGroupMessageResponse.class); + case 401: + throw new InvalidUnidentifiedAccessHeaderException(); + case 404: + throw new NotFoundException("At least one unregistered user in message send."); + case 409: + GroupMismatchedDevices[] mismatchedDevices = readBodyJson(response.body(), GroupMismatchedDevices[].class); + throw new GroupMismatchedDevicesException(mismatchedDevices); + case 410: + GroupStaleDevices[] staleDevices = readBodyJson(response.body(), GroupStaleDevices[].class); + throw new GroupStaleDevicesException(staleDevices); + case 508: + throw new ServerRejectedException(); + default: + throw new NonSuccessfulResponseCodeException(response.code()); + } + } + public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional unidentifiedAccess) throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java new file mode 100644 index 0000000000..e021087274 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SendGroupMessageResponse.java @@ -0,0 +1,31 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class SendGroupMessageResponse { + + @JsonProperty + private String[] uuids404; + + public SendGroupMessageResponse() {} + + public Set getUnsentTargets() { + Set uuids = new HashSet<>(uuids404.length); + + for (String raw : uuids404) { + Optional parsed = UuidUtil.parse(raw); + if (parsed.isPresent()) { + uuids.add(parsed.get()); + } + } + + return uuids; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupMismatchedDevicesException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupMismatchedDevicesException.java new file mode 100644 index 0000000000..d3faba07d9 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupMismatchedDevicesException.java @@ -0,0 +1,24 @@ +package org.whispersystems.signalservice.internal.push.exceptions; + +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.internal.push.GroupMismatchedDevices; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a 409 response from the service during a sender key send. + */ +public class GroupMismatchedDevicesException extends NonSuccessfulResponseCodeException { + + private final List mismatchedDevices; + + public GroupMismatchedDevicesException(GroupMismatchedDevices[] mismatchedDevices) { + super(409); + this.mismatchedDevices = Arrays.asList(mismatchedDevices); + } + + public List getMismatchedDevices() { + return mismatchedDevices; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupStaleDevicesException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupStaleDevicesException.java new file mode 100644 index 0000000000..3740bbcf30 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/GroupStaleDevicesException.java @@ -0,0 +1,24 @@ +package org.whispersystems.signalservice.internal.push.exceptions; + +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.internal.push.GroupStaleDevices; + +import java.util.Arrays; +import java.util.List; + +/** + * Represents a 410 response from the service during a sender key send. + */ +public class GroupStaleDevicesException extends NonSuccessfulResponseCodeException { + + private final List staleDevices; + + public GroupStaleDevicesException(GroupStaleDevices[] staleDevices) { + super(410); + this.staleDevices = Arrays.asList(staleDevices); + } + + public List getStaleDevices() { + return staleDevices; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/InvalidUnidentifiedAccessHeaderException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/InvalidUnidentifiedAccessHeaderException.java new file mode 100644 index 0000000000..7e638eb72c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/exceptions/InvalidUnidentifiedAccessHeaderException.java @@ -0,0 +1,14 @@ +package org.whispersystems.signalservice.internal.push.exceptions; + +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; + +/** + * Indicates that the unidentified authorization header provided to the multi_recipient endpoint + * was incorrect (i.e. one or more of your unauthorized access keys is invalid); + */ +public class InvalidUnidentifiedAccessHeaderException extends NonSuccessfulResponseCodeException { + + public InvalidUnidentifiedAccessHeaderException() { + super(401); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java index b7ed93abfa..5d12ef797e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java @@ -1,5 +1,8 @@ package org.whispersystems.signalservice.internal.serialize; +import com.google.protobuf.ByteString; + +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceMetadata; import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto; @@ -9,15 +12,20 @@ public final class SignalServiceMetadataProtobufSerializer { } public static MetadataProto toProtobuf(SignalServiceMetadata metadata) { - return MetadataProto.newBuilder() - .setAddress(SignalServiceAddressProtobufSerializer.toProtobuf(metadata.getSender())) - .setSenderDevice(metadata.getSenderDevice()) - .setNeedsReceipt(metadata.isNeedsReceipt()) - .setTimestamp(metadata.getTimestamp()) - .setServerReceivedTimestamp(metadata.getServerReceivedTimestamp()) - .setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp()) - .setServerGuid(metadata.getServerGuid()) - .build(); + MetadataProto.Builder builder = MetadataProto.newBuilder() + .setAddress(SignalServiceAddressProtobufSerializer.toProtobuf(metadata.getSender())) + .setSenderDevice(metadata.getSenderDevice()) + .setNeedsReceipt(metadata.isNeedsReceipt()) + .setTimestamp(metadata.getTimestamp()) + .setServerReceivedTimestamp(metadata.getServerReceivedTimestamp()) + .setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp()) + .setServerGuid(metadata.getServerGuid()); + + if (metadata.getGroupId().isPresent()) { + builder.setGroupId(ByteString.copyFrom(metadata.getGroupId().get())); + } + + return builder.build(); } public static SignalServiceMetadata fromProtobuf(MetadataProto metadata) { @@ -27,6 +35,7 @@ public final class SignalServiceMetadataProtobufSerializer { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.getNeedsReceipt(), - metadata.getServerGuid()); + metadata.getServerGuid(), + Optional.fromNullable(metadata.getGroupId()).transform(ByteString::toByteArray)); } } diff --git a/libsignal/service/src/main/proto/InternalSerialization.proto b/libsignal/service/src/main/proto/InternalSerialization.proto index 1004e74699..58488ffc65 100644 --- a/libsignal/service/src/main/proto/InternalSerialization.proto +++ b/libsignal/service/src/main/proto/InternalSerialization.proto @@ -42,6 +42,7 @@ message MetadataProto { optional int64 serverDeliveredTimestamp = 6; optional bool needsReceipt = 4; optional string serverGuid = 7; + optional bytes groupId = 8; } message AddressProto { diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index e24c081d66..192bc408b3 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -18,6 +18,8 @@ message Envelope { PREKEY_BUNDLE = 3; RECEIPT = 5; UNIDENTIFIED_SENDER = 6; + reserved 7; // SENDERKEY_MESSAGE + PLAINTEXT_CONTENT = 8; } optional Type type = 1; @@ -33,12 +35,14 @@ message Envelope { } message Content { - optional DataMessage dataMessage = 1; - optional SyncMessage syncMessage = 2; - optional CallMessage callMessage = 3; - optional NullMessage nullMessage = 4; - optional ReceiptMessage receiptMessage = 5; - optional TypingMessage typingMessage = 6; + optional DataMessage dataMessage = 1; + optional SyncMessage syncMessage = 2; + optional CallMessage callMessage = 3; + optional NullMessage nullMessage = 4; + optional ReceiptMessage receiptMessage = 5; + optional TypingMessage typingMessage = 6; + optional bytes senderKeyDistributionMessage = 7; + optional bytes decryptionErrorMessage = 8; } message CallMessage { @@ -612,3 +616,9 @@ message PaymentAddress { optional bytes signature = 2; } } + +message DecryptionErrorMessage { + optional bytes ratchetKey = 1; + optional uint64 timestamp = 2; + optional uint32 deviceId = 3; +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java index 4ff895d450..f058ce7c01 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java @@ -16,7 +16,7 @@ public final class AccountAttributesTest { "reglock1234", new byte[10], false, - new AccountAttributes.Capabilities(true, true, true, true), + new AccountAttributes.Capabilities(true, true, true, true, true), false)); assertEquals("{\"signalingKey\":\"skey\"," + "\"registrationId\":123," + @@ -28,19 +28,19 @@ public final class AccountAttributesTest { "\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," + "\"unrestrictedUnidentifiedAccess\":false," + "\"discoverableByPhoneNumber\":false," + - "\"capabilities\":{\"uuid\":true,\"storage\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json); + "\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"gv2-3\":true,\"gv1-migration\":true}}", json); } @Test public void gv2_true() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"gv2-3\":true,\"gv1-migration\":false}", json); + String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false)); + assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":true,\"gv1-migration\":false}", json); } @Test public void gv2_false() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"gv2-3\":false,\"gv1-migration\":false}", json); + String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false)); + assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"gv2-3\":false,\"gv1-migration\":false}", json); } } \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevicesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevicesTest.java new file mode 100644 index 0000000000..ac8292fd98 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupMismatchedDevicesTest.java @@ -0,0 +1,40 @@ +package org.whispersystems.signalservice.internal.push; + +import org.junit.Test; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class GroupMismatchedDevicesTest { + + @Test + public void testSimpleParse() throws IOException { + GroupMismatchedDevices[] parsed = JsonUtil.fromJson("[\n" + + " {\n" + + " \"uuid\": \"12345678-1234-1234-1234-123456789012\",\n" + + " \"devices\": {\n" + + " \"missingDevices\": [1, 2],\n" + + " \"extraDevices\": [3]\n" + + " }\n" + + " },\n" + + " {\n" + + " \"uuid\": \"22345678-1234-1234-1234-123456789012\",\n" + + " \"devices\": {\n" + + " \"missingDevices\": [],\n" + + " \"extraDevices\": [2]\n" + + " }\n" + + " }\n" + + "]", GroupMismatchedDevices[].class); + + assertEquals(2, parsed.length); + assertEquals("12345678-1234-1234-1234-123456789012", parsed[0].getUuid()); + assertEquals(1, (int) parsed[0].getDevices().getMissingDevices().get(0)); + assertEquals(2, (int) parsed[0].getDevices().getMissingDevices().get(1)); + assertEquals(3, (int) parsed[0].getDevices().getExtraDevices().get(0)); + + assertEquals("22345678-1234-1234-1234-123456789012", parsed[1].getUuid()); + assertEquals(2, (int) parsed[1].getDevices().getExtraDevices().get(0)); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupStaleDevicesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupStaleDevicesTest.java new file mode 100644 index 0000000000..08232ec26d --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/push/GroupStaleDevicesTest.java @@ -0,0 +1,36 @@ +package org.whispersystems.signalservice.internal.push; + +import org.junit.Test; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class GroupStaleDevicesTest { + + @Test + public void testSimpleParse() throws IOException { + GroupStaleDevices[] parsed = JsonUtil.fromJson("[\n" + + " {\n" + + " \"uuid\": \"12345678-1234-1234-1234-123456789012\",\n" + + " \"devices\": {\n" + + " \"staleDevices\": [3]\n" + + " }\n" + + " },\n" + + " {\n" + + " \"uuid\": \"22345678-1234-1234-1234-123456789012\",\n" + + " \"devices\": {\n" + + " \"staleDevices\": [2]\n" + + " }\n" + + " }\n" + + "]", GroupStaleDevices[].class); + + assertEquals(2, parsed.length); + assertEquals("12345678-1234-1234-1234-123456789012", parsed[0].getUuid()); + assertEquals(3, (int) parsed[0].getDevices().getStaleDevices().get(0)); + + assertEquals("22345678-1234-1234-1234-123456789012", parsed[1].getUuid()); + assertEquals(2, (int) parsed[1].getDevices().getStaleDevices().get(0)); + } +} diff --git a/libsignal/service/witness-verifications.gradle b/libsignal/service/witness-verifications.gradle index a2ebe02129..bb091529ab 100644 --- a/libsignal/service/witness-verifications.gradle +++ b/libsignal/service/witness-verifications.gradle @@ -30,7 +30,7 @@ dependencyVerification { ['org.threeten:threetenbp:1.3.6', 'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'], - ['org.whispersystems:signal-client-java:0.5.1', - '682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'], + ['org.whispersystems:signal-client-java:0.8.1', + '6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'], ] }