Initial pre-alpha support for sender key.

This commit is contained in:
Greyson Parrelli
2021-05-14 14:03:35 -04:00
parent c54f016213
commit 57c0b8fd0f
124 changed files with 3668 additions and 444 deletions
@@ -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);
}
}
@@ -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);
@@ -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);
@@ -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,
@@ -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()
}
}
@@ -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,
)
@@ -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 {
@@ -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();
}
}
@@ -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()) {
@@ -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);
@@ -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();
}
@@ -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()) {
@@ -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();
}
}
}
@@ -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<RecipientId, Optional<UnidentifiedAccessPair>> getAccessMapFor(@NonNull Context context, @NonNull List<Recipient> recipients) {
List<Optional<UnidentifiedAccessPair>> accessList = getAccessFor(context, recipients, true);
Iterator<Recipient> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccessPair>> accessIterator = accessList.iterator();
Map<RecipientId, Optional<UnidentifiedAccessPair>> accessMap = new HashMap<>(recipients.size());
while (recipientIterator.hasNext()) {
accessMap.put(recipientIterator.next().getId(), accessIterator.next());
}
return accessMap;
}
@WorkerThread
public static List<Optional<UnidentifiedAccessPair>> getAccessFor(@NonNull Context context, @NonNull List<Recipient> recipients, boolean log) {
byte[] ourUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey());
@@ -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<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
return sessionStore.loadExistingSessions(addresses);
}
@Override
public List<Integer> 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<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId) {
return senderKeyStore.getSenderKeySharedWith(distributionId);
}
@Override
public void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
senderKeyStore.markSenderKeySharedWith(distributionId, addresses);
}
@Override
public void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
senderKeyStore.clearSenderKeySharedWith(distributionId, addresses);
}
}
@@ -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<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId) {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
return DatabaseFactory.getSenderKeySharedDatabase(context).getSharedWith(distributionId);
}
}
@Override
public void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses) {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
DatabaseFactory.getSenderKeySharedDatabase(context).markAsShared(distributionId, addresses);
}
}
@Override
public void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> 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();
}
}
}
@@ -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;
}
@@ -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<SessionRecord> loadExistingSessions(List<SignalProtocolAddress> addresses) throws NoSessionException {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
List<RecipientDevice> ids = addresses.stream()
.map(address -> new RecipientDevice(RecipientId.fromExternalPush(address.getName()), address.getDeviceId()))
.collect(Collectors.toList());
List<SessionRecord> 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()) {
@@ -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,
@@ -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<String> 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<RecipientId> 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<RecipientId> 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<UUID> 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<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
private static @NonNull List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
List<RecipientId> groupMembers = new ArrayList<>(uuids.size());
for (UUID uuid : uuids) {
@@ -751,11 +803,9 @@ public final class GroupDatabase extends Database {
return groupMembers;
}
private static List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
List<RecipientId> recipientIds = uuidsToRecipientIds(uuids);
return recipientIds;
private static @NonNull List<RecipientId> getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
return uuidsToRecipientIds(uuids);
}
public @NonNull List<GroupId.V2> 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();
}
@@ -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)});
@@ -158,7 +158,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract Optional<InsertResult> insertMessageInbox(IncomingMediaMessage retrieved, String contentLocation, long threadId) throws MmsException;
public abstract Pair<Long, Long> insertMessageInbox(@NonNull NotificationInd notification, int subscriptionId);
public abstract Optional<InsertResult> 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;
@@ -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();
}
@@ -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;
}
@@ -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));
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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<SignalProtocolAddress> 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<SignalProtocolAddress> getSharedWith(@NonNull DistributionId distributionId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = DISTRIBUTION_ID + " = ?";
String[] args = SqlUtil.buildArgs(distributionId);
Set<SignalProtocolAddress> 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<SignalProtocolAddress> 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);
}
}
@@ -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<SessionRecord> load(@NonNull List<RecipientDevice> ids) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<SessionRecord> 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<SessionRow> getAllFor(@NonNull RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
List<SessionRow> 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;
}
}
}
@@ -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)
@@ -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();
@@ -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));
@@ -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() {
@@ -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
)
@@ -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));
@@ -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();
}
}
@@ -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;
@@ -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());
}
@@ -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<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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());
@@ -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<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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));
}
}
@@ -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));
}
@@ -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);
}
@@ -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));
}
@@ -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
@@ -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) {
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
}
@@ -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
@@ -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
@@ -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<UnidentifiedAccessPair> 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());
@@ -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<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.IMPLICIT, dataMessage.build());
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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<byte[]> 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);
@@ -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<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> completions = deliver(destinations);
GroupId.V2 groupId = GroupId.v2(GroupUtil.requireMasterKey(groupContextV2.getMasterKey().toByteArray()));
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
List<Recipient> 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<Recipient> deliver(@NonNull List<Recipient> destinations)
private @NonNull List<Recipient> deliver(@NonNull List<Recipient> destinations, @NonNull GroupId.V2 groupId)
throws IOException, UntrustedIdentityException
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
SignalServiceGroupV2 group = SignalServiceGroupV2.fromProtobuf(groupContextV2);
SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(timestamp)
.asGroupMessage(group)
.build();
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, groupDataMessage);
List<SendMessageResult> results = GroupSendUtil.sendDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage);
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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
@@ -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<UnidentifiedAccessPair> 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);
@@ -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<UnidentifiedAccessPair> 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);
@@ -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<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, @NonNull Recipient targetAuthor, long targetSentTimestamp)
throws IOException, UntrustedIdentityException
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> 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<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
List<SendMessageResult> results;
if (conversationRecipient.isPushV2Group()) {
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
} else {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
}
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, long targetSentTimestamp)
throws IOException, UntrustedIdentityException
{
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> 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<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
List<SendMessageResult> results;
if (conversationRecipient.isPushV2Group()) {
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
} else {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
}
return GroupSendJobHelper.getCompletedSends(context, results);
}
@@ -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
@@ -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> groupId;
private final DecryptionErrorMessage errorMessage;
public SendRetryReceiptJob(@NonNull RecipientId recipientId, @NonNull Optional<GroupId> 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> 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<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient);
Optional<byte[]> 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<SendRetryReceiptJob> {
@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> 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);
}
}
}
}
@@ -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<SignalServiceAddress> address = Collections.singletonList(RecipientUtil.toSignalServiceAddress(context, recipient));
DistributionId distributionId = groupDatabase.getOrCreateDistributionId(groupId);
SenderKeyDistributionMessage message = messageSender.getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> 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<SenderKeyDistributionSendJob> {
@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);
}
}
}
@@ -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<Recipient> recipients = Collections.singletonList(recipient);
Optional<byte[]> groupId = Optional.absent();
List<Recipient> recipients = Collections.singletonList(recipient);
Optional<byte[]> 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<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
List<Optional<UnidentifiedAccessPair>> 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<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
List<Optional<UnidentifiedAccessPair>> 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!");
}
@@ -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.
@@ -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");
}
}
@@ -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<SendMessageResult> sendDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> 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<SendMessageResult> sendTypingMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> 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<SendMessageResult> sendMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
@NonNull SendOperation sendOperation,
@Nullable CancelationSignal cancelationSignal)
throws IOException, UntrustedIdentityException
{
RecipientData recipients = new RecipientData(context, allTargets);
List<Recipient> senderKeyTargets = new LinkedList<>();
List<Recipient> legacyTargets = new LinkedList<>();
for (Recipient recipient : allTargets) {
Optional<UnidentifiedAccessPair> 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<SendMessageResult> 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<SignalServiceAddress> targets = senderKeyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<UnidentifiedAccess> access = senderKeyTargets.stream().map(r -> recipients.requireAccess(r.getId())).collect(Collectors.toList());
List<SendMessageResult> 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<SignalServiceAddress> targets = legacyTargets.stream().map(r -> recipients.getAddress(r.getId())).collect(Collectors.toList());
List<Optional<UnidentifiedAccessPair>> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList());
boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0;
List<SendMessageResult> 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<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
@NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access,
boolean isRecipientUpdate)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException;
@NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> 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<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
@NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> access,
boolean isRecipientUpdate)
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException
{
return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message);
}
@Override
public @NonNull List<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> 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<SendMessageResult> sendWithSenderKey(@NonNull SignalServiceMessageSender messageSender,
@NonNull DistributionId distributionId,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<UnidentifiedAccess> 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<SendMessageResult> sendLegacy(@NonNull SignalServiceMessageSender messageSender,
@NonNull List<SignalServiceAddress> targets,
@NonNull List<Optional<UnidentifiedAccessPair>> 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<RecipientId, Optional<UnidentifiedAccessPair>> accessById;
private final Map<RecipientId, SignalServiceAddress> addressById;
RecipientData(@NonNull Context context, @NonNull List<Recipient> 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<UnidentifiedAccessPair> 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<RecipientId, SignalServiceAddress> mapAddresses(@NonNull Context context, @NonNull List<Recipient> recipients) throws IOException {
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients);
Iterator<Recipient> recipientIterator = recipients.iterator();
Iterator<SignalServiceAddress> addressIterator = addresses.iterator();
Map<RecipientId, SignalServiceAddress> map = new HashMap<>(recipients.size());
while (recipientIterator.hasNext()) {
map.put(recipientIterator.next().getId(), addressIterator.next());
}
return map;
}
}
}
@@ -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());
@@ -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<SignalServiceAttachment> attachments = message.getAttachments().or(Collections.emptyList());
@@ -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> 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
{
@@ -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;
}
@@ -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;
}
@@ -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;
@@ -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;
@@ -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<PendingRetryReceiptModel> {
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();
}
}
}
@@ -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<String, Object> 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) {