Initial pre-alpha support for sender key.

fork-5.53.8
Greyson Parrelli 2021-05-14 14:03:35 -04:00
rodzic c54f016213
commit 57c0b8fd0f
124 zmienionych plików z 3668 dodań i 444 usunięć

Wyświetl plik

@ -376,7 +376,7 @@ dependencies {
implementation project(':device-transfer')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.5.1'
implementation 'org.whispersystems:signal-client-android:0.8.0'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation('com.mobilecoin:android-sdk:1.0.0') {

Wyświetl plik

@ -736,6 +736,8 @@
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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,

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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,
)

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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());

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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,

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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)});

Wyświetl plik

@ -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;

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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();

Wyświetl plik

@ -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));

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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
)

Wyświetl plik

@ -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));

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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());
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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());

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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());

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}
}
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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!");
}

Wyświetl plik

@ -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.

Wyświetl plik

@ -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");
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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());

Wyświetl plik

@ -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());

Wyświetl plik

@ -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
{

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="110dp"
android:viewportWidth="200"
android:viewportHeight="110">
<path
android:pathData="M39.3,15.1L12.9,15C9.1,15 6,18.1 6,21.9L5.9,88C5.9,91.8 9,94.9 12.8,94.9L39.1,95C42.9,95 46,91.9 46,88.1L46.2,22C46.2,18.2 43.1,15.1 39.3,15.1ZM42.8,88.1C42.8,90.1 41.1,91.8 39.1,91.8L12.7,91.7C10.7,91.7 9,90 9,88L9.1,21.9C9.1,19.9 10.8,18.2 12.8,18.2L39.2,18.3C41.2,18.3 42.9,20 42.9,22L42.8,88.1Z"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M187.3,15.1L160.9,15C157.1,15 154,18.1 154,21.9L153.9,88C153.9,91.8 157,94.9 160.8,94.9L187.2,95C191,95 194.1,91.9 194.1,88.1L194.2,22C194.2,18.2 191.1,15.1 187.3,15.1ZM190.8,88.1C190.8,90.1 189.1,91.8 187.1,91.8L160.7,91.7C158.7,91.7 157,90 157,88L157.1,21.9C157.1,19.9 158.8,18.2 160.8,18.2L187.2,18.3C189.2,18.3 190.9,20 190.9,22L190.8,88.1Z"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M126,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M136,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M146,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M54,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M64,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M74,56m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#C6C6C6"/>
<path
android:pathData="M100,56m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
android:fillColor="#FFC207"/>
<path
android:pathData="M102.083,47H97.917L98.75,58.25H101.25L102.083,47Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M102.079,61.5C102.353,61.87 102.5,62.305 102.5,62.75C102.5,63.347 102.237,63.919 101.768,64.341C101.299,64.763 100.663,65 100,65C99.506,65 99.022,64.868 98.611,64.621C98.2,64.374 97.88,64.022 97.69,63.611C97.501,63.2 97.452,62.748 97.548,62.311C97.645,61.875 97.883,61.474 98.232,61.159C98.582,60.844 99.027,60.63 99.512,60.543C99.997,60.456 100.5,60.501 100.957,60.671C101.414,60.842 101.804,61.13 102.079,61.5Z"
android:fillColor="#ffffff"/>
</vector>

Wyświetl plik

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical"
android:padding="?attr/dialogPreferredPadding">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="70dp"
android:layout_marginEnd="70dp"
android:layout_gravity="center"
app:srcCompat="@drawable/chat_session_refresh" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:text="@string/BadDecryptLearnMoreDialog_delivery_issue"
android:textAppearance="@style/TextAppearance.Signal.Title2" />
<TextView
android:id="@+id/bad_decrypt_dialog_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Signal.Body1"
tools:text="@string/BadDecryptLearnMoreDialog_couldnt_be_delivered_individual" />
</LinearLayout>

Wyświetl plik

@ -1212,6 +1212,7 @@
<string name="MessageRecord_you_marked_your_safety_number_with_s_verified_from_another_device">You marked your safety number with %s verified from another device</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified">You marked your safety number with %s unverified</string>
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device">You marked your safety number with %s unverified from another device</string>
<string name="MessageRecord_a_message_from_s_couldnt_be_delivered">A message from %s couldn\'t be delivered</string>
<!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
@ -1625,6 +1626,7 @@
<string name="ThreadRecord_you_marked_verified">You marked verified</string>
<string name="ThreadRecord_you_marked_unverified">You marked unverified</string>
<string name="ThreadRecord_message_could_not_be_processed">Message could not be processed</string>
<string name="ThreadRecord_delivery_issue">Delivery issue</string>
<string name="ThreadRecord_message_request">Message Request</string>
<string name="ThreadRecord_photo">Photo</string>
<string name="ThreadRecord_gif">GIF</string>
@ -2071,6 +2073,11 @@
<string name="prompt_mms_activity__signal_requires_mms_settings_to_deliver_media_and_group_messages">Signal requires MMS settings to deliver media and group messages through your wireless carrier. Your device does not make this information available, which is occasionally true for locked devices and other restrictive configurations.</string>
<string name="prompt_mms_activity__to_send_media_and_group_messages_tap_ok">To send media and group messages, tap \'OK\' and complete the requested settings. The MMS settings for your carrier can generally be located by searching for \'your carrier APN\'. You will only need to do this once.</string>
<!-- BadDecryptLearnMoreDialog -->
<string name="BadDecryptLearnMoreDialog_delivery_issue">Delivery Issue</string>
<string name="BadDecryptLearnMoreDialog_couldnt_be_delivered_individual">A message, sticker, reaction, or read receipt couldn\'t be delivered to you from %s. They may have tried sending it to you directly, or in a group.</string>
<string name="BadDecryptLearnMoreDialog_couldnt_be_delivered_group">A message, sticker, reaction, or read receipt couldn\'t be delivered to you from %s.</string>
<!-- profile_create_activity -->
<string name="CreateProfileActivity_first_name_required">First name (required)</string>
<string name="CreateProfileActivity_last_name_optional">Last name (optional)</string>
@ -2464,6 +2471,13 @@
<string name="preferences__internal_delete_all_dynamic_shortcuts" translatable="false">Delete all dynamic shortcuts</string>
<string name="preferences__internal_click_to_delete_all_dynamic_shortcuts" translatable="false">Click to delete all dynamic shortcuts</string>
<string name="preferences__internal_disable_profile_sharing" translatable="false">Disable Profile Sharing</string>
<string name="preferences__internal_sender_key" translatable="false">Sender Key</string>
<string name="preferences__internal_clear_all_state" translatable="false">Clear all state</string>
<string name="preferences__internal_click_to_delete_all_sender_key_state" translatable="false">Click to delete all sender key state</string>
<string name="preferences__internal_clear_shared_state" translatable="false">Clear shared state</string>
<string name="preferences__internal_click_to_delete_all_sharing_state" translatable="false">Click to delete all sharing state</string>
<string name="preferences__internal_remove_two_person_minimum" translatable="false">Remove 2 person minimum</string>
<string name="preferences__internal_remove_the_requirement_that_you_need" translatable="false">Remove the requirement that you need at least 2 recipients to use sender key.</string>
<!-- Payments -->
<string name="PaymentsActivityFragment__all_activity">All activity</string>

Wyświetl plik

@ -3,6 +3,9 @@
dependencyVerification {
verify = [
['androidx.activity:activity-ktx:1.1.0',
'1996c36d3d2d62db5020b8ec634b5f854b1a698960c3552e1a00c69221baeabe'],
['androidx.activity:activity:1.1.0',
'4f2b35916768032f7d0c20e250e28b29037ed4ce9ebf3da4fcd51bcb0c6067ef'],
@ -48,6 +51,9 @@ dependencyVerification {
['androidx.cardview:cardview:1.0.0',
'1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7'],
['androidx.collection:collection-ktx:1.1.0',
'2bfc54475c047131913361f56d0f7f019c6e5bee53eeb0eb7d94a7c499a05227'],
['androidx.collection:collection:1.1.0',
'632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72'],
@ -63,6 +69,9 @@ dependencyVerification {
['androidx.coordinatorlayout:coordinatorlayout:1.1.0',
'44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb'],
['androidx.core:core-ktx:1.1.0',
'070cc5f8864f449128a2f4b25ca5b67aa3adca3ee1bd611e2eaf1a18fad83178'],
['androidx.core:core:1.3.2',
'94de196cd67950cff6ef3e1ac59015f8eaaf61840bdc238f2cf54ddef8dd0be9'],
@ -84,6 +93,9 @@ dependencyVerification {
['androidx.exifinterface:exifinterface:1.0.0',
'ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11'],
['androidx.fragment:fragment-ktx:1.2.5',
'50f0f3b734f93829eeac7456b7cb13e5430741e555c535911a958ee4a8242bca'],
['androidx.fragment:fragment:1.2.5',
'd19e82d142def6c4e136da70bf92f194c0ecc61d14ab4e84567b2ced0920fa93'],
@ -117,6 +129,9 @@ dependencyVerification {
['androidx.lifecycle:lifecycle-extensions:2.1.0',
'bd53c64b038585215b4959c1a388437a3ad525608a31c58e4283c3e371727d4d'],
['androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0',
'5951f882e95b7e05ceb9adfca0fa2ebd511d63ea5a00da4eae6c6d0c1903da18'],
['androidx.lifecycle:lifecycle-livedata-core:2.2.0',
'556c1f3af90aa9d7d0d330565adbf6da71b2429148bac91e07c485f4f9abf614'],
@ -126,12 +141,18 @@ dependencyVerification {
['androidx.lifecycle:lifecycle-process:2.1.0',
'8cddd0c7f4927bbf71fb71fca000786df82cc597c99463d6916ccbe4a205a9ac'],
['androidx.lifecycle:lifecycle-runtime-ktx:2.2.0',
'c29fc87694e6ce116b61207221e53ed285862a6628055790b0bcf9ce45d8cc68'],
['androidx.lifecycle:lifecycle-runtime:2.2.0',
'2f866c07a1f33a8c9bb69a9545d4f20b4f0628cd0a155432386d7cb081e1e0bc'],
['androidx.lifecycle:lifecycle-service:2.1.0',
'23516745f34f16ff7850bb1eadd55cf193dd789cba428de4bca120433e3bfd69'],
['androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0',
'f791001f2211947e56ad3d96d12c9ae93fc5589b88f08603f69a2265c9a7d702'],
['androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0',
'3ce866fb822b20fe2f188f974992869a0a6233fe40acbefcff090d6def5e7f33'],
@ -474,6 +495,12 @@ dependencyVerification {
['org.jetbrains.kotlin:kotlin-stdlib:1.4.32',
'13e9fd3e69dc7230ce0fc873a92a4e5d521d179bcf1bef75a6705baac3bfecba'],
['org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0',
'c80aaadf041f044d324a19a73f88879dfd1e4d026b14e3230075ff9081942ae3'],
['org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0',
'6f3a60fea2403b80385b399952aeb3a4cf0985a45b8da04b6f31825171901a1d'],
['org.jetbrains:annotations:13.0',
'ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478'],
@ -510,11 +537,11 @@ dependencyVerification {
['org.threeten:threetenbp:1.3.6',
'f4c23ffaaed717c3b99c003e0ee02d6d66377fd47d866fec7d971bd8644fc1a7'],
['org.whispersystems:signal-client-android:0.5.1',
'c5b523163612fedcf71a5fb51a85cb5a4f545329ec6cf7d4fc885e830466c2e3'],
['org.whispersystems:signal-client-android:0.8.0',
'f18e705717d463ecbaae3dedf0747b1fabfcfc98b708cc309200960678f230cc'],
['org.whispersystems:signal-client-java:0.5.1',
'682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'],
['org.whispersystems:signal-client-java:0.8.1',
'6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'],
['pl.tajchert:waitingdots:0.1.0',
'2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c'],

Wyświetl plik

@ -28,7 +28,7 @@ dependencyVerification {
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation project(':core-util')
implementation 'org.whispersystems:signal-client-java:0.5.1'
implementation 'org.whispersystems:signal-client-java:0.8.1'
api 'org.greenrobot:eventbus:3.0.0'
testImplementation 'junit:junit:4.12'

Wyświetl plik

@ -81,7 +81,7 @@ dependencyVerification {
['org.greenrobot:eventbus:3.0.0',
'180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c'],
['org.whispersystems:signal-client-java:0.5.1',
'682a8094d38a91c8759071b77177ed8196a7137314fdfbb17e819c9ca57a0397'],
['org.whispersystems:signal-client-java:0.8.1',
'6bcf9ab3a77be20b43086fd802d9ade3940f36ed7b99bac2a79b9bcaf0a7808b'],
]
}

Wyświetl plik

@ -21,7 +21,7 @@ dependencies {
api 'com.googlecode.libphonenumber:libphonenumber:8.12.17'
api 'com.fasterxml.jackson.core:jackson-databind:2.9.9.2'
api 'org.whispersystems:signal-client-java:0.5.1'
implementation 'org.whispersystems:signal-client-java:0.8.1'
api 'com.squareup.okhttp3:okhttp:3.12.10'
implementation 'org.threeten:threetenbp:1.3.6'

Wyświetl plik

@ -31,6 +31,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
@ -46,6 +47,7 @@ import java.io.IOException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -175,6 +177,33 @@ public class SignalServiceMessagePipe {
}
}
public Future<SendGroupMessageResponse> sendToGroup(byte[] body, byte[] joinedUnidentifiedAccess, long timestamp, boolean online) throws IOException {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/vnd.signal-messenger.mrm");
add("Unidentified-Access-Key:" + Base64.encodeBytes(joinedUnidentifiedAccess));
}};
String path = String.format(Locale.US, "/v1/messages/multi_recipient?ts=%s&online=%s", timestamp, online);
WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder()
.setId(new SecureRandom().nextLong())
.setVerb("PUT")
.setPath(path)
.addAllHeaders(headers)
.setBody(ByteString.copyFrom(body))
.build();
ListenableFuture<WebsocketResponse> response = websocket.sendRequest(requestMessage);
return FutureTransformers.map(response, value -> {
if (value.getStatus() == 200) {
return JsonUtil.fromJson(value.getBody(), SendGroupMessageResponse.class);
} else {
throw new NonSuccessfulResponseCodeException(value.getStatus());
}
});
}
public Future<SendMessageResponse> send(OutgoingPushMessageList list, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
List<String> headers = new LinkedList<String>() {{
add("content-type:application/json");

Wyświetl plik

@ -8,15 +8,26 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.SessionBuilder;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.groups.GroupSessionBuilder;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.libsignal.protocol.PlaintextContent;
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.EnvelopeContent;
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
@ -28,6 +39,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
@ -51,6 +63,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
@ -62,14 +75,17 @@ import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserExce
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.Uint64RangeException;
import org.whispersystems.signalservice.api.util.Uint64Util;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes;
import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes;
import org.whispersystems.signalservice.internal.push.GroupMismatchedDevices;
import org.whispersystems.signalservice.internal.push.GroupStaleDevices;
import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse;
import org.whispersystems.signalservice.internal.push.MismatchedDevices;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList;
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
import org.whispersystems.signalservice.internal.push.ProvisioningProtos;
import org.whispersystems.signalservice.internal.push.PushAttachmentData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@ -86,6 +102,8 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMe
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Verified;
import org.whispersystems.signalservice.internal.push.StaleDevices;
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory;
@ -94,17 +112,20 @@ import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec;
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import org.whispersystems.util.ByteArrayUtil;
import org.whispersystems.util.FlagUtil;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -114,6 +135,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* The main interface for sending Signal Service messages.
@ -205,9 +227,26 @@ public class SignalServiceMessageSender {
SignalServiceReceiptMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createReceiptContent(message);
Content content = createReceiptContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, null);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null);
}
/**
* Send a retry receipt for a bad-encrypted envelope.
*/
public void sendRetryReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
Optional<byte[]> groupId,
DecryptionErrorMessage errorMessage)
throws IOException, UntrustedIdentityException
{
PlaintextContent content = new PlaintextContent(errorMessage);
EnvelopeContent envelopeContent = EnvelopeContent.plaintext(content, groupId);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
}
/**
@ -223,9 +262,10 @@ public class SignalServiceMessageSender {
SignalServiceTypingMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createTypingContent(message);
Content content = createTypingContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, null);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null);
}
public void sendTyping(List<SignalServiceAddress> recipients,
@ -234,8 +274,23 @@ public class SignalServiceMessageSender {
CancelationSignal cancelationSignal)
throws IOException
{
byte[] content = createTypingContent(message);
sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, cancelationSignal);
Content content = createTypingContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, cancelationSignal);
}
/**
* Send a typing indicator a group. Doesn't bother with return results, since these are best-effort.
*/
public void sendGroupTyping(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
SignalServiceTypingMessage message)
throws IOException, UntrustedIdentityException, InvalidKeyException, NoSessionException
{
Content content = createTypingContent(message);
sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, ContentHint.IMPLICIT, message.getGroupId().orNull(), false);
}
@ -251,8 +306,10 @@ public class SignalServiceMessageSender {
SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createCallContent(message);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null);
Content content = createCallContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.DEFAULT, Optional.absent());
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
}
/**
@ -277,18 +334,22 @@ public class SignalServiceMessageSender {
* @throws UntrustedIdentityException
* @throws IOException
*/
public SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceDataMessage message)
public SendMessageResult sendDataMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
ContentHint contentHint,
SignalServiceDataMessage message)
throws UntrustedIdentityException, IOException
{
byte[] content = createMessageContent(message);
long timestamp = message.getTimestamp();
SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null);
Content content = createMessageContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
long timestamp = message.getTimestamp();
SendMessageResult result = sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
if (result.getSuccess() != null && result.getSuccess().isNeedsSync()) {
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, null);
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.of(recipient), timestamp, Collections.singletonList(result), false);
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null);
}
// TODO [greyson][session] Delete this when we delete the button
@ -309,21 +370,81 @@ public class SignalServiceMessageSender {
}
/**
* Send a message to a group.
* Gives you a {@link SenderKeyDistributionMessage} that can then be sent out to recipients to tell them about your sender key.
* Will create a sender key session for the provided DistributionId if one doesn't exist.
*/
public SenderKeyDistributionMessage getOrCreateNewGroupSession(DistributionId distributionId) {
SignalProtocolAddress self = new SignalProtocolAddress(localAddress.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID);
return new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).create(self, distributionId.asUuid());
}
/**
* Sends the provided {@link SenderKeyDistributionMessage} to the specified recipients.
*/
public List<SendMessageResult> sendSenderKeyDistributionMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SenderKeyDistributionMessage message,
byte[] groupId)
throws IOException
{
ByteString distributionBytes = ByteString.copyFrom(message.serialize());
Content content = Content.newBuilder().setSenderKeyDistributionMessage(distributionBytes).build();
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.of(groupId));
long timestamp = System.currentTimeMillis();
return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
}
/**
* Processes an inbound {@link SenderKeyDistributionMessage}.
*/
public void processSenderKeyDistributionMessage(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) {
new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).process(sender, senderKeyDistributionMessage);
}
/**
* Sends a {@link SignalServiceDataMessage} to a group using sender keys.
*/
public List<SendMessageResult> sendGroupDataMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
boolean isRecipientUpdate,
ContentHint contentHint,
SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException
{
Content content = createMessageContent(message);
Optional<byte[]> groupId = message.getGroupId();
List<SendMessageResult> results = sendGroupMessage(distributionId, recipients, unidentifiedAccess, message.getTimestamp(), content, contentHint, groupId.orNull(), false);
if (isMultiDevice.get()) {
Content syncMessage = createMultiDeviceSentTranscriptContent(content, Optional.absent(), message.getTimestamp(), results, isRecipientUpdate);
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null);
}
return results;
}
/**
* Sends a message to a group using client-side fanout.
*
* @param recipients The group members.
* @param message The group message.
* @throws IOException
*/
public List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
boolean isRecipientUpdate,
SignalServiceDataMessage message)
public List<SendMessageResult> sendDataMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
boolean isRecipientUpdate,
ContentHint contentHint,
SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createMessageContent(message);
Content content = createMessageContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId());
long timestamp = message.getTimestamp();
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, null);
List<SendMessageResult> results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null);
boolean needsSyncInResults = false;
for (SendMessageResult result : results) {
@ -339,17 +460,19 @@ public class SignalServiceMessageSender {
recipient = Optional.of(recipients.get(0));
}
byte[] syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate);
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, null);
Content syncMessage = createMultiDeviceSentTranscriptContent(content, recipient, timestamp, results, isRecipientUpdate);
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), timestamp, syncMessageContent, false, null);
}
return results;
}
public void sendMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
public void sendSyncMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
byte[] content;
Content content;
if (message.getContacts().isPresent()) {
content = createMultiDeviceContactsContent(message.getContacts().get().getContactsStream().asStream(),
@ -379,7 +502,7 @@ public class SignalServiceMessageSender {
} else if (message.getKeys().isPresent()) {
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
} else if (message.getVerified().isPresent()) {
sendMessage(message.getVerified().get(), unidentifiedAccess);
sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess);
return;
} else {
throw new IOException("Unsupported sync message!");
@ -388,7 +511,9 @@ public class SignalServiceMessageSender {
long timestamp = message.getSent().isPresent() ? message.getSent().get().getTimestamp()
: System.currentTimeMillis();
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), timestamp, content, false, null);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), timestamp, envelopeContent, false, null);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
@ -506,7 +631,7 @@ public class SignalServiceMessageSender {
attachment.getUploadTimestamp());
}
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
private void sendVerifiedMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
byte[] nullMessageBody = DataMessage.newBuilder()
@ -518,16 +643,19 @@ public class SignalServiceMessageSender {
.setPadding(ByteString.copyFrom(nullMessageBody))
.build();
byte[] content = Content.newBuilder()
Content content = Content.newBuilder()
.setNullMessage(nullMessage)
.build()
.toByteArray();
.build();
SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, false, null);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
SendMessageResult result = sendMessage(message.getDestination(), getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, false, null);
if (result.getSuccess().isNeedsSync()) {
byte[] syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray());
sendMessage(localAddress, Optional.<UnidentifiedAccess>absent(), message.getTimestamp(), syncMessage, false, null);
Content syncMessage = createMultiDeviceVerifiedContent(message, nullMessage.toByteArray());
EnvelopeContent syncMessageContent = EnvelopeContent.encrypted(syncMessage, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null);
}
}
@ -543,15 +671,16 @@ public class SignalServiceMessageSender {
.setPadding(ByteString.copyFrom(nullMessageBody))
.build();
byte[] content = Content.newBuilder()
.setNullMessage(nullMessage)
.build()
.toByteArray();
Content content = Content.newBuilder()
.setNullMessage(nullMessage)
.build();
return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, null);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
return sendMessage(address, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), envelopeContent, false, null);
}
private byte[] createTypingContent(SignalServiceTypingMessage message) {
private Content createTypingContent(SignalServiceTypingMessage message) {
Content.Builder container = Content.newBuilder();
TypingMessage.Builder builder = TypingMessage.newBuilder();
@ -565,10 +694,10 @@ public class SignalServiceMessageSender {
builder.setGroupId(ByteString.copyFrom(message.getGroupId().get()));
}
return container.setTypingMessage(builder).build().toByteArray();
return container.setTypingMessage(builder).build();
}
private byte[] createReceiptContent(SignalServiceReceiptMessage message) {
private Content createReceiptContent(SignalServiceReceiptMessage message) {
Content.Builder container = Content.newBuilder();
ReceiptMessage.Builder builder = ReceiptMessage.newBuilder();
@ -580,10 +709,10 @@ public class SignalServiceMessageSender {
else if (message.isReadReceipt()) builder.setType(ReceiptMessage.Type.READ);
else if (message.isViewedReceipt()) builder.setType(ReceiptMessage.Type.VIEWED);
return container.setReceiptMessage(builder).build().toByteArray();
return container.setReceiptMessage(builder).build();
}
private byte[] createMessageContent(SignalServiceDataMessage message) throws IOException {
private Content createMessageContent(SignalServiceDataMessage message) throws IOException {
Content.Builder container = Content.newBuilder();
DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments());
@ -779,10 +908,10 @@ public class SignalServiceMessageSender {
builder.setTimestamp(message.getTimestamp());
return enforceMaxContentSize(container.setDataMessage(builder).build().toByteArray());
return enforceMaxContentSize(container.setDataMessage(builder).build());
}
private byte[] createCallContent(SignalServiceCallMessage callMessage) {
private Content createCallContent(SignalServiceCallMessage callMessage) {
Content.Builder container = Content.newBuilder();
CallMessage.Builder builder = CallMessage.newBuilder();
@ -862,30 +991,30 @@ public class SignalServiceMessageSender {
}
container.setCallMessage(builder);
return container.build().toByteArray();
return container.build();
}
private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException {
private Content createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) throws IOException {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
builder.setContacts(SyncMessage.Contacts.newBuilder()
.setBlob(createAttachmentPointer(contacts))
.setComplete(complete));
return container.setSyncMessage(builder).build().toByteArray();
return container.setSyncMessage(builder).build();
}
private byte[] createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException {
private Content createMultiDeviceGroupsContent(SignalServiceAttachmentStream groups) throws IOException {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
builder.setGroups(SyncMessage.Groups.newBuilder()
.setBlob(createAttachmentPointer(groups)));
return container.setSyncMessage(builder).build().toByteArray();
return container.setSyncMessage(builder).build();
}
private byte[] createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
SignalServiceAddress address = transcript.getDestination().get();
SendMessageResult result = SendMessageResult.success(address, unidentifiedAccess.isPresent(), true, -1);
@ -896,60 +1025,56 @@ public class SignalServiceMessageSender {
false);
}
private byte[] createMultiDeviceSentTranscriptContent(byte[] content, Optional<SignalServiceAddress> recipient,
long timestamp, List<SendMessageResult> sendMessageResults,
boolean isRecipientUpdate)
private Content createMultiDeviceSentTranscriptContent(Content content, Optional<SignalServiceAddress> recipient,
long timestamp, List<SendMessageResult> sendMessageResults,
boolean isRecipientUpdate)
{
try {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
DataMessage dataMessage = Content.parseFrom(content).getDataMessage();
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Sent.Builder sentMessage = SyncMessage.Sent.newBuilder();
DataMessage dataMessage = content.getDataMessage();
sentMessage.setTimestamp(timestamp);
sentMessage.setMessage(dataMessage);
sentMessage.setTimestamp(timestamp);
sentMessage.setMessage(dataMessage);
for (SendMessageResult result : sendMessageResults) {
if (result.getSuccess() != null) {
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder();
for (SendMessageResult result : sendMessageResults) {
if (result.getSuccess() != null) {
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder builder = SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder();
if (result.getAddress().getUuid().isPresent()) {
builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString());
}
if (result.getAddress().getNumber().isPresent()) {
builder = builder.setDestinationE164(result.getAddress().getNumber().get());
}
builder.setUnidentified(result.getSuccess().isUnidentified());
sentMessage.addUnidentifiedStatus(builder.build());
if (result.getAddress().getUuid().isPresent()) {
builder = builder.setDestinationUuid(result.getAddress().getUuid().get().toString());
}
if (result.getAddress().getNumber().isPresent()) {
builder = builder.setDestinationE164(result.getAddress().getNumber().get());
}
builder.setUnidentified(result.getSuccess().isUnidentified());
sentMessage.addUnidentifiedStatus(builder.build());
}
if (recipient.isPresent()) {
if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString());
if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get());
}
if (dataMessage.getExpireTimer() > 0) {
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
}
if (dataMessage.getIsViewOnce()) {
dataMessage = dataMessage.toBuilder().clearAttachments().build();
sentMessage.setMessage(dataMessage);
}
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build().toByteArray();
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
if (recipient.isPresent()) {
if (recipient.get().getUuid().isPresent()) sentMessage.setDestinationUuid(recipient.get().getUuid().get().toString());
if (recipient.get().getNumber().isPresent()) sentMessage.setDestinationE164(recipient.get().getNumber().get());
}
if (dataMessage.getExpireTimer() > 0) {
sentMessage.setExpirationStartTimestamp(System.currentTimeMillis());
}
if (dataMessage.getIsViewOnce()) {
dataMessage = dataMessage.toBuilder().clearAttachments().build();
sentMessage.setMessage(dataMessage);
}
sentMessage.setIsRecipientUpdate(isRecipientUpdate);
return container.setSyncMessage(syncMessage.setSent(sentMessage)).build();
}
private byte[] createMultiDeviceReadContent(List<ReadMessage> readMessages) {
private Content createMultiDeviceReadContent(List<ReadMessage> readMessages) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
@ -967,10 +1092,10 @@ public class SignalServiceMessageSender {
builder.addRead(readBuilder.build());
}
return container.setSyncMessage(builder).build().toByteArray();
return container.setSyncMessage(builder).build();
}
private byte[] createMultiDeviceViewedContent(List<ViewedMessage> readMessages) {
private Content createMultiDeviceViewedContent(List<ViewedMessage> readMessages) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
@ -988,10 +1113,10 @@ public class SignalServiceMessageSender {
builder.addViewed(viewedBuilder.build());
}
return container.setSyncMessage(builder).build().toByteArray();
return container.setSyncMessage(builder).build();
}
private byte[] createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) {
private Content createMultiDeviceViewOnceOpenContent(ViewOnceOpenMessage readMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder builder = createSyncMessageBuilder();
SyncMessage.ViewOnceOpen.Builder viewOnceBuilder = SyncMessage.ViewOnceOpen.newBuilder().setTimestamp(readMessage.getTimestamp());
@ -1006,10 +1131,10 @@ public class SignalServiceMessageSender {
builder.setViewOnceOpen(viewOnceBuilder.build());
return container.setSyncMessage(builder).build().toByteArray();
return container.setSyncMessage(builder).build();
}
private byte[] createMultiDeviceBlockedContent(BlockedListMessage blocked) {
private Content createMultiDeviceBlockedContent(BlockedListMessage blocked) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Blocked.Builder blockedMessage = SyncMessage.Blocked.newBuilder();
@ -1027,10 +1152,10 @@ public class SignalServiceMessageSender {
blockedMessage.addGroupIds(ByteString.copyFrom(groupId));
}
return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build().toByteArray();
return container.setSyncMessage(syncMessage.setBlocked(blockedMessage)).build();
}
private byte[] createMultiDeviceConfigurationContent(ConfigurationMessage configuration) {
private Content createMultiDeviceConfigurationContent(ConfigurationMessage configuration) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Configuration.Builder configurationMessage = SyncMessage.Configuration.newBuilder();
@ -1053,10 +1178,10 @@ public class SignalServiceMessageSender {
configurationMessage.setProvisioningVersion(ProvisioningProtos.ProvisioningVersion.CURRENT_VALUE);
return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build().toByteArray();
return container.setSyncMessage(syncMessage.setConfiguration(configurationMessage)).build();
}
private byte[] createMultiDeviceStickerPackOperationContent(List<StickerPackOperationMessage> stickerPackOperations) {
private Content createMultiDeviceStickerPackOperationContent(List<StickerPackOperationMessage> stickerPackOperations) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
@ -1081,10 +1206,10 @@ public class SignalServiceMessageSender {
syncMessage.addStickerPackOperation(builder);
}
return container.setSyncMessage(syncMessage).build().toByteArray();
return container.setSyncMessage(syncMessage).build();
}
private byte[] createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) {
private Content createMultiDeviceFetchTypeContent(SignalServiceSyncMessage.FetchType fetchType) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.FetchLatest.Builder fetchMessage = SyncMessage.FetchLatest.newBuilder();
@ -1101,10 +1226,10 @@ public class SignalServiceMessageSender {
break;
}
return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build().toByteArray();
return container.setSyncMessage(syncMessage.setFetchLatest(fetchMessage)).build();
}
private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
private Content createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder();
@ -1143,10 +1268,10 @@ public class SignalServiceMessageSender {
syncMessage.setMessageRequestResponse(responseMessage);
return container.setSyncMessage(syncMessage).build().toByteArray();
return container.setSyncMessage(syncMessage).build();
}
private byte[] createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) {
private Content createMultiDeviceOutgoingPaymentContent(OutgoingPaymentMessage message) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.OutgoingPayment.Builder paymentMessage = SyncMessage.OutgoingPayment.newBuilder();
@ -1180,10 +1305,10 @@ public class SignalServiceMessageSender {
syncMessage.setOutgoingPayment(paymentMessage);
return container.setSyncMessage(syncMessage).build().toByteArray();
return container.setSyncMessage(syncMessage).build();
}
private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
private Content createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Keys.Builder builder = SyncMessage.Keys.newBuilder();
@ -1194,10 +1319,10 @@ public class SignalServiceMessageSender {
Log.w(TAG, "Invalid keys message!");
}
return container.setSyncMessage(syncMessage.setKeys(builder)).build().toByteArray();
return container.setSyncMessage(syncMessage.setKeys(builder)).build();
}
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
private Content createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
Verified.Builder verifiedMessageBuilder = Verified.newBuilder();
@ -1221,7 +1346,7 @@ public class SignalServiceMessageSender {
}
syncMessage.setVerified(verifiedMessageBuilder);
return container.setSyncMessage(syncMessage).build().toByteArray();
return container.setSyncMessage(syncMessage).build();
}
private SyncMessage.Builder createSyncMessageBuilder() {
@ -1389,7 +1514,7 @@ public class SignalServiceMessageSender {
private List<SendMessageResult> sendMessage(List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccess>> unidentifiedAccess,
long timestamp,
byte[] content,
EnvelopeContent content,
boolean online,
CancelationSignal cancelationSignal)
throws IOException
@ -1461,7 +1586,7 @@ public class SignalServiceMessageSender {
private SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] content,
EnvelopeContent content,
boolean online,
CancelationSignal cancelationSignal)
throws UntrustedIdentityException, IOException
@ -1491,7 +1616,7 @@ public class SignalServiceMessageSender {
return SendMessageResult.success(recipient, false, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime);
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, e);
Log.w(TAG, "[sendMessage] Pipe failed, falling back...");
Log.w(TAG, "[sendMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
}
} else if (unidentifiedPipe.isPresent() && unidentifiedAccess.isPresent()) {
try {
@ -1530,7 +1655,190 @@ public class SignalServiceMessageSender {
}
}
throw new IOException("Failed to resolve conflicts after 3 attempts!");
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
}
/**
* Will send a message using sender keys to all of the specified recipients. It is assumed that
* all of the recipients have UUIDs.
*
* This method will handle sending out SenderKeyDistributionMessages as necessary.
*/
private List<SendMessageResult> sendGroupMessage(DistributionId distributionId,
List<SignalServiceAddress> recipients,
List<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
Content content,
ContentHint contentHint,
byte[] groupId,
boolean online)
throws IOException, UntrustedIdentityException, NoSessionException, InvalidKeyException
{
if (recipients.isEmpty()) {
Log.w(TAG, "[sendGroupMessage] Empty recipient list!");
return Collections.emptyList();
}
Preconditions.checkArgument(recipients.size() == unidentifiedAccess.size(), "Unidentified access mismatch!");
if (recipients.stream().anyMatch(r -> !r.getUuid().isPresent())) {
throw new IllegalArgumentException("All recipients must have a UUID!");
}
Map<UUID, UnidentifiedAccess> accessByUuid = new HashMap<>();
Iterator<SignalServiceAddress> addressIterator = recipients.iterator();
Iterator<UnidentifiedAccess> accessIterator = unidentifiedAccess.iterator();
while (addressIterator.hasNext()) {
accessByUuid.put(addressIterator.next().getUuid().get(), accessIterator.next());
}
for (int i = 0; i < RETRY_COUNT; i++) {
List<SignalProtocolAddress> destinations = new LinkedList<>();
for (SignalServiceAddress recipient : recipients) {
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), SignalServiceAddress.DEFAULT_DEVICE_ID));
for (int deviceId : store.getSubDeviceSessions(recipient.getIdentifier())) {
if (store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) {
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), deviceId));
}
}
}
Set<SignalProtocolAddress> sharedWith = store.getSenderKeySharedWith(distributionId);
List<SignalServiceAddress> needsSenderKey = destinations.stream()
.filter(a -> !sharedWith.contains(a))
.map(a -> UuidUtil.parseOrThrow(a.getName()))
.distinct()
.map(uuid -> new SignalServiceAddress(uuid, null))
.collect(Collectors.toList());
if (needsSenderKey.size() > 0) {
Log.i(TAG, "[sendGroupMessage] Need to send the distribution message to " + needsSenderKey.size() + " addresses.");
SenderKeyDistributionMessage message = getOrCreateNewGroupSession(distributionId);
List<Optional<UnidentifiedAccessPair>> access = needsSenderKey.stream()
.map(r -> {
UnidentifiedAccess targetAccess = accessByUuid.get(r.getUuid().get());
return Optional.of(new UnidentifiedAccessPair(targetAccess, targetAccess));
})
.collect(Collectors.toList());
List<SendMessageResult> results = sendSenderKeyDistributionMessage(needsSenderKey, access, message, groupId);
List<SignalServiceAddress> successes = results.stream()
.filter(SendMessageResult::isSuccess)
.map(SendMessageResult::getAddress)
.collect(Collectors.toList());
Set<String> successUuids = successes.stream().map(a -> a.getUuid().get().toString()).collect(Collectors.toSet());
Set<SignalProtocolAddress> successAddresses = destinations.stream().filter(a -> successUuids.contains(a.getName())).collect(Collectors.toSet());;
store.markSenderKeySharedWith(distributionId, successAddresses);
Log.i(TAG, "[sendGroupMessage] Successfully sent sender keys to " + successes.size() + "/" + needsSenderKey.size() + " recipients.");
int failureCount = results.size() - successes.size();
if (failureCount > 0) {
Log.w(TAG, "[sendGroupMessage] Failed to send sender keys to " + failureCount + " recipients. Sending back failed results now.");
List<SendMessageResult> trueFailures = results.stream()
.filter(r -> !r.isSuccess())
.collect(Collectors.toList());
Set<SignalServiceAddress> failedAddresses = trueFailures.stream()
.map(SendMessageResult::getAddress)
.collect(Collectors.toSet());
List<SendMessageResult> fakeNetworkFailures = recipients.stream()
.filter(r -> !failedAddresses.contains(r))
.map(SendMessageResult::networkFailure)
.collect(Collectors.toList());
List<SendMessageResult> modifiedResults = new LinkedList<>();
modifiedResults.addAll(trueFailures);
modifiedResults.addAll(fakeNetworkFailures);
return modifiedResults;
}
}
SignalServiceCipher cipher = new SignalServiceCipher(localAddress, store, sessionLock, null);
SenderCertificate senderCertificate = unidentifiedAccess.get(0).getUnidentifiedCertificate();
byte[] ciphertext;
try {
ciphertext = cipher.encryptForGroup(distributionId, destinations, senderCertificate, content.toByteArray(), contentHint, groupId);
} catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
throw new UntrustedIdentityException("Untrusted during group encrypt", e.getName(), e.getUntrustedIdentity());
}
byte[] joinedUnidentifiedAccess = new byte[16];
for (UnidentifiedAccess access : unidentifiedAccess) {
joinedUnidentifiedAccess = ByteArrayUtil.xor(joinedUnidentifiedAccess, access.getUnidentifiedAccessKey());
}
Optional<SignalServiceMessagePipe> pipe = this.unidentifiedPipe.get();
if (pipe.isPresent()) {
try {
SendGroupMessageResponse response = pipe.get().sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online).get(10, TimeUnit.SECONDS);
return transformGroupResponseToMessageResults(recipients, response);
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, "[sendGroupMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
}
} else {
Log.d(TAG, "[sendGroupMessage] No pipe available.");
}
try {
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online);
return transformGroupResponseToMessageResults(recipients, response);
} catch (GroupMismatchedDevicesException e) {
Log.w(TAG, "[sendGroupMessage] Handling mismatched devices.", e);
for (GroupMismatchedDevices mismatched : e.getMismatchedDevices()) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(mismatched.getUuid()), Optional.absent());
handleMismatchedDevices(socket, address, mismatched.getDevices());
List<SignalProtocolAddress> clearAddresses = mismatched.getDevices().getExtraDevices().stream()
.map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
.collect(Collectors.toList());
store.clearSenderKeySharedWith(distributionId, clearAddresses);
}
} catch (GroupStaleDevicesException e) {
Log.w(TAG, "[sendGroupMessage] Handling stale devices.", e);
for (GroupStaleDevices stale : e.getStaleDevices()) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parse(stale.getUuid()), Optional.absent());
handleStaleDevices(address, stale.getDevices());
List<SignalProtocolAddress> clearAddresses = stale.getDevices().getStaleDevices().stream()
.map(device -> new SignalProtocolAddress(address.getIdentifier(), device))
.collect(Collectors.toList());
store.clearSenderKeySharedWith(distributionId, clearAddresses);
}
}
Log.w(TAG, "[sendGroupMessage] Attempt failed (i = " + i + ")");
}
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
}
private List<SendMessageResult> transformGroupResponseToMessageResults(List<SignalServiceAddress> recipients, SendGroupMessageResponse response) {
Set<UUID> unregistered = response.getUnsentTargets();
List<SendMessageResult> failures = unregistered.stream()
.map(uuid -> new SignalServiceAddress(uuid, null))
.map(SendMessageResult::unregisteredFailure)
.collect(Collectors.toList());
List<SendMessageResult> success = recipients.stream()
.filter(r -> !unregistered.contains(r.getUuid().get()))
.map(a -> SendMessageResult.success(a, true, isMultiDevice.get(), -1))
.collect(Collectors.toList());
List<SendMessageResult> results = new LinkedList<>(success);
results.addAll(failures);
return results;
}
private List<AttachmentPointer> createAttachmentPointers(Optional<List<SignalServiceAttachment>> attachments) throws IOException {
@ -1625,7 +1933,7 @@ public class SignalServiceMessageSender {
SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] plaintext,
EnvelopeContent plaintext,
boolean online)
throws IOException, InvalidKeyException, UntrustedIdentityException
{
@ -1648,7 +1956,7 @@ public class SignalServiceMessageSender {
SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
int deviceId,
byte[] plaintext)
EnvelopeContent plaintext)
throws IOException, InvalidKeyException, UntrustedIdentityException
{
SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);
@ -1742,9 +2050,20 @@ public class SignalServiceMessageSender {
return results;
}
private byte[] enforceMaxContentSize(byte[] content) {
if (maxEnvelopeSize > 0 && content.length > maxEnvelopeSize) {
throw new ContentTooLargeException(content.length);
private EnvelopeContent enforceMaxContentSize(EnvelopeContent content) {
int size = content.size();
if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) {
throw new ContentTooLargeException(size);
}
return content;
}
private Content enforceMaxContentSize(Content content) {
int size = content.toByteArray().length;
if (maxEnvelopeSize > 0 && size > maxEnvelopeSize) {
throw new ContentTooLargeException(size);
}
return content;
}

Wyświetl plik

@ -6,5 +6,5 @@ import org.whispersystems.libsignal.state.SignalProtocolStore;
* And extension of the normal protocol store interface that has additional methods that are needed
* in the service layer, but not the protocol layer.
*/
public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore {
public interface SignalServiceProtocolStore extends SignalProtocolStore, SignalServiceSessionStore, SignalServiceSenderKeyStore {
}

Wyświetl plik

@ -0,0 +1,29 @@
package org.whispersystems.signalservice.api;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.groups.state.SenderKeyStore;
import org.whispersystems.signalservice.api.push.DistributionId;
import java.util.Collection;
import java.util.Set;
/**
* And extension of the normal protocol sender key store interface that has additional methods that are
* needed in the service layer, but not the protocol layer.
*/
public interface SignalServiceSenderKeyStore extends SenderKeyStore {
/**
* @return A set of protocol addresses that have previously been sent the sender key data for the provided distributionId.
*/
Set<SignalProtocolAddress> getSenderKeySharedWith(DistributionId distributionId);
/**
* Marks the provided addresses as having been sent the sender key data for the provided distributionId.
*/
void markSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses);
/**
* Marks the provided addresses as not knowing about the provided distributionId.
*/
void clearSenderKeySharedWith(DistributionId distributionId, Collection<SignalProtocolAddress> addresses);
}

Wyświetl plik

@ -128,14 +128,18 @@ public class AccountAttributes {
@JsonProperty("gv1-migration")
private boolean gv1Migration;
@JsonProperty
private boolean senderKey;
@JsonCreator
public Capabilities() {}
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration) {
public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey) {
this.uuid = uuid;
this.gv2 = gv2;
this.storage = storage;
this.gv1Migration = gv1Migration;
this.senderKey = senderKey;
}
public boolean isUuid() {
@ -153,5 +157,9 @@ public class AccountAttributes {
public boolean isGv1Migration() {
return gv1Migration;
}
public boolean isSenderKey() {
return senderKey;
}
}
}

Wyświetl plik

@ -0,0 +1,38 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
import java.util.HashMap;
import java.util.Map;
public enum ContentHint {
/** This message has content, but you shouldnt expect it to be re-sent to you. */
DEFAULT(UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT),
/** You should expect to be able to have this content be re-sent to you. */
RESENDABLE(UnidentifiedSenderMessageContent.CONTENT_HINT_RESENDABLE),
/** This message has no real content and likely cannot be re-sent to you. */
IMPLICIT(UnidentifiedSenderMessageContent.CONTENT_HINT_IMPLICIT);
private static final Map<Integer, ContentHint> TYPE_MAP = new HashMap<>();
static {
for (ContentHint value : values()) {
TYPE_MAP.put(value.getType(), value);
}
}
private final int type;
ContentHint(int type) {
this.type = type;
}
public int getType() {
return type;
}
public static ContentHint fromType(int type) {
return TYPE_MAP.getOrDefault(type, DEFAULT);
}
}

Wyświetl plik

@ -0,0 +1,156 @@
package org.whispersystems.signalservice.api.crypto;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.UntrustedIdentityException;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.libsignal.protocol.DecryptionErrorMessage;
import org.whispersystems.libsignal.protocol.PlaintextContent;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.push.PushTransportDetails;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope.Type;
import org.whispersystems.util.Base64;
/**
* An abstraction over the different types of message contents we can have.
*/
public interface EnvelopeContent {
/**
* Processes the content using sealed sender.
*/
OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
SignalSealedSessionCipher sealedSessionCipher,
SignalProtocolAddress destination,
SenderCertificate senderCertificate)
throws UntrustedIdentityException, InvalidKeyException;
/**
* Processes the content using unsealed sender.
*/
OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException;
/**
* An estimated size, in bytes.
*/
int size();
/**
* Wrap {@link Content} you plan on sending as an encrypted message.
* This is the default. Consider anything else exceptional.
*/
static EnvelopeContent encrypted(Content content, ContentHint contentHint, Optional<byte[]> groupId) {
return new Encrypted(content.toByteArray(), contentHint, groupId);
}
/**
* Wraps a {@link PlaintextContent}. This is exceptional, currently limited only to {@link DecryptionErrorMessage}.
*/
static EnvelopeContent plaintext(PlaintextContent content, Optional<byte[]> groupId) {
return new Plaintext(content, groupId);
}
class Encrypted implements EnvelopeContent {
private final byte[] unpaddedMessage;
private final ContentHint contentHint;
private final Optional<byte[]> groupId;
public Encrypted(byte[] unpaddedMessage, ContentHint contentHint, Optional<byte[]> groupId) {
this.unpaddedMessage = unpaddedMessage;
this.contentHint = contentHint;
this.groupId = groupId;
}
@Override
public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
SignalSealedSessionCipher sealedSessionCipher,
SignalProtocolAddress destination,
SenderCertificate senderCertificate)
throws UntrustedIdentityException, InvalidKeyException
{
PushTransportDetails transportDetails = new PushTransportDetails();
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message,
senderCertificate,
contentHint.getType(),
groupId);
byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent);
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
}
@Override
public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException {
PushTransportDetails transportDetails = new PushTransportDetails();
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
String body = Base64.encodeBytes(message.serialize());
int type;
switch (message.getType()) {
case CiphertextMessage.PREKEY_TYPE: type = Type.PREKEY_BUNDLE_VALUE; break;
case CiphertextMessage.WHISPER_TYPE: type = Type.CIPHERTEXT_VALUE; break;
default: throw new AssertionError("Bad type: " + message.getType());
}
return new OutgoingPushMessage(type, destination.getDeviceId(), remoteRegistrationId, body);
}
@Override
public int size() {
return unpaddedMessage.length;
}
}
class Plaintext implements EnvelopeContent {
private final PlaintextContent plaintextContent;
private final Optional<byte[]> groupId;
public Plaintext(PlaintextContent plaintextContent, Optional<byte[]> groupId) {
this.plaintextContent = plaintextContent;
this.groupId = groupId;
}
@Override
public OutgoingPushMessage processSealedSender(SignalSessionCipher sessionCipher,
SignalSealedSessionCipher sealedSessionCipher,
SignalProtocolAddress destination,
SenderCertificate senderCertificate)
throws UntrustedIdentityException, InvalidKeyException
{
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(plaintextContent,
senderCertificate,
ContentHint.IMPLICIT.getType(),
groupId);
byte[] ciphertext = sealedSessionCipher.encrypt(destination, messageContent);
String body = Base64.encodeBytes(ciphertext);
int remoteRegistrationId = sealedSessionCipher.getRemoteRegistrationId(destination);
return new OutgoingPushMessage(Type.UNIDENTIFIED_SENDER_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
}
@Override
public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) {
String body = Base64.encodeBytes(plaintextContent.getBody());
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
return new OutgoingPushMessage(Type.PLAINTEXT_CONTENT_VALUE, destination.getDeviceId(), remoteRegistrationId, body);
}
@Override
public int size() {
return plaintextContent.getBody().length;
}
}
}

Wyświetl plik

@ -0,0 +1,39 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.DuplicateMessageException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.LegacyMessageException;
import org.whispersystems.libsignal.NoSessionException;
import org.whispersystems.libsignal.groups.GroupCipher;
import org.whispersystems.libsignal.protocol.CiphertextMessage;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.util.UUID;
/**
* A thread-safe wrapper around {@link GroupCipher}.
*/
public class SignalGroupCipher {
private final SignalSessionLock lock;
private final GroupCipher cipher;
public SignalGroupCipher(SignalSessionLock lock, GroupCipher cipher) {
this.lock = lock;
this.cipher = cipher;
}
public CiphertextMessage encrypt(UUID distributionId, byte[] paddedPlaintext) throws NoSessionException {
try (SignalSessionLock.Lock unused = lock.acquire()) {
return cipher.encrypt(distributionId, paddedPlaintext);
}
}
public byte[] decrypt(byte[] senderKeyMessageBytes)
throws LegacyMessageException, DuplicateMessageException, InvalidMessageException, NoSessionException
{
try (SignalSessionLock.Lock unused = lock.acquire()) {
return cipher.decrypt(senderKeyMessageBytes);
}
}
}

Wyświetl plik

@ -0,0 +1,35 @@
package org.whispersystems.signalservice.api.crypto;
import org.whispersystems.libsignal.SessionBuilder;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.groups.GroupSessionBuilder;
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.util.UUID;
/**
* A thread-safe wrapper around {@link SessionBuilder}.
*/
public class SignalGroupSessionBuilder {
private final SignalSessionLock lock;
private final GroupSessionBuilder builder;
public SignalGroupSessionBuilder(SignalSessionLock lock, GroupSessionBuilder builder) {
this.lock = lock;
this.builder = builder;
}
public void process(SignalProtocolAddress sender, SenderKeyDistributionMessage senderKeyDistributionMessage) {
try (SignalSessionLock.Lock unused = lock.acquire()) {
builder.process(sender, senderKeyDistributionMessage);
}
}
public SenderKeyDistributionMessage create(SignalProtocolAddress sender, UUID distributionId) {
try (SignalSessionLock.Lock unused = lock.acquire()) {
return builder.create(sender, distributionId);
}
}
}

Some files were not shown because too many files have changed in this diff Show More