diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt index cbcd25293f..8681134ae3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsFragment.kt @@ -296,6 +296,18 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa SignalDatabase.senderKeyShared.deleteAllFor(group.distributionId) } + override fun clearSenderKeyAndArchiveSessions(recipientId: RecipientId) { + clearSenderKey(recipientId) + + val group = SignalDatabase.groups.getGroup(recipientId).orNull() + if (group == null) { + Log.w(TAG, "Couldn't find group for recipientId: $recipientId") + return + } + + group.members.forEach { archiveSessions(it) } + } + class InternalViewModel( val recipientId: RecipientId ) : ViewModel(), RecipientForeverObserver { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsScreen.kt index 7200c32ec6..de96e727c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/InternalConversationSettingsScreen.kt @@ -33,6 +33,7 @@ private enum class Dialog { NONE, DISABLE_PROFILE_SHARING, CLEAR_SENDER_KEY, + CLEAR_SENDER_KEY_AND_ARCHIVE_SESSIONS, DELETE_SESSIONS, ARCHIVE_SESSIONS, DELETE_AVATAR, @@ -207,6 +208,16 @@ fun InternalConversationSettingsScreen( } ) } + + item { + Rows.TextRow( + text = "Clear sender key and archive sessions", + label = "Resets any sender key state and archives all sessions for group members, will force creating new sessions and re-distributing sender key material.", + onClick = { + dialog = Dialog.CLEAR_SENDER_KEY_AND_ARCHIVE_SESSIONS + } + ) + } } else { item { Rows.TextRow( @@ -375,6 +386,9 @@ private fun rememberOnConfirm( Dialog.CLEAR_SENDER_KEY -> { { callbacks.clearSenderKey(state.recipientId) } } + Dialog.CLEAR_SENDER_KEY_AND_ARCHIVE_SESSIONS -> { + { callbacks.clearSenderKeyAndArchiveSessions(state.recipientId) } + } } } } @@ -473,6 +487,7 @@ interface InternalConversationSettingsScreenCallbacks { fun splitAndCreateThreads(recipientId: RecipientId) = Unit fun splitWithoutCreatingThreads(recipientId: RecipientId) = Unit fun clearSenderKey(recipientId: RecipientId) = Unit + fun clearSenderKeyAndArchiveSessions(recipientId: RecipientId) = Unit object Empty : InternalConversationSettingsScreenCallbacks } diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 300b2399a4..8922314ee1 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -5,7 +5,10 @@ */ package org.whispersystems.signalservice.api; +import org.signal.core.models.ServiceId; +import org.signal.core.models.ServiceId.PNI; import org.signal.core.util.Base64; +import org.signal.core.util.UuidUtil; import org.signal.libsignal.metadata.certificate.SenderCertificate; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKeyPair; @@ -68,8 +71,6 @@ 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.push.DistributionId; -import org.signal.core.models.ServiceId; -import org.signal.core.models.ServiceId.PNI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; @@ -84,7 +85,6 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Uint64RangeException; import org.whispersystems.signalservice.api.util.Uint64Util; -import org.signal.core.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; @@ -718,7 +718,7 @@ public class SignalServiceMessageSender { boolean urgent = false; if (!aciStore.isMultiDevice()) { - Log.w(TAG, "We do not have any linked devices. Skipping."); + Log.d(TAG, "We do not have any linked devices. Skipping."); return SendMessageResult.success(localAddress, Collections.emptyList(), false, false, 0, Optional.empty()); } @@ -2113,7 +2113,10 @@ public class SignalServiceMessageSender { Log.d(TAG, "[" + timestamp + "] Sending to " + recipients.size() + " recipients."); enforceMaxEnvelopeContentSize(content); - long startTime = System.currentTimeMillis(); + long startTime = System.currentTimeMillis(); + + eagerlyFetchMissingPreKeys(recipients, sealedSenderAccesses, story); + List> singleResults = new LinkedList<>(); Iterator recipientIterator = recipients.iterator(); Iterator sealedSenderAccessIterator = sealedSenderAccesses.iterator(); @@ -2819,6 +2822,81 @@ public class SignalServiceMessageSender { } } + private void eagerlyFetchMissingPreKeys(List recipients, List sealedSenderAccesses, boolean story) { + long start = System.currentTimeMillis(); + + Iterator recipientIterator = recipients.iterator(); + Iterator sealedSenderAccessIterator = sealedSenderAccesses.iterator(); + List> eagerFetches = new LinkedList<>(); + + while (recipientIterator.hasNext()) { + SignalServiceAddress recipient = recipientIterator.next(); + SealedSenderAccess sealedSenderAccess = sealedSenderAccessIterator.next(); + SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID); + + if (!aciStore.containsSession(signalProtocolAddress)) { + Observable thing = Single.fromCallable(() -> { + eagerlyFetchMissingPreKeys(recipient, sealedSenderAccess, story); + return true; + }) + .subscribeOn(scheduler) + .toObservable(); + + eagerFetches.add(thing); + } + } + + if (eagerFetches.isEmpty()) { + return; + } + + Log.i(TAG, "[eagerPrefetch] Attempting to fetch prekeys for " + eagerFetches.size() + " recipients"); + + try { + //noinspection ResultOfMethodCallIgnored + Observable.mergeDelayError(eagerFetches, Integer.MAX_VALUE, 1) + .observeOn(scheduler) + .lastOrError() + .blockingGet(); + } catch (RuntimeException e) { + Log.w(TAG, "[eagerPrefetch] Unexpectedly failed eager fetching prekeys", e); + return; + } + + Log.i(TAG, "[eagerPrefetch] Completed in " + (System.currentTimeMillis() - start) + "ms"); + } + + private void eagerlyFetchMissingPreKeys(SignalServiceAddress recipient, SealedSenderAccess sealedSenderAccess, boolean story) { + SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), SignalServiceAddress.DEFAULT_DEVICE_ID); + + try { + List preKeys = getPreKeys(recipient, sealedSenderAccess, SignalServiceAddress.DEFAULT_DEVICE_ID, story); + + for (PreKeyBundle preKey : preKeys) { + Log.d(TAG, "[eagerFetch] Initializing prekey session for " + signalProtocolAddress); + + try { + SignalProtocolAddress preKeyAddress = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId()); + SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(aciStore, preKeyAddress)); + sessionBuilder.process(preKey); + } catch (org.signal.libsignal.protocol.UntrustedIdentityException e) { + Log.i(TAG, "[eagerPrefetch] Untrusted identity for recipient"); + return; + + } + } + + if (eventListener.isPresent()) { + eventListener.get().onSecurityEvent(recipient); + } + } catch (IOException e) { + Log.i(TAG, "[eagerPrefetch] Network issue encountered"); + } catch (InvalidKeyException e) { + Log.i(TAG, "[eagerPrefetch] Invalid pre-key"); + return; + } + } + private List getPreKeys(SignalServiceAddress recipient, @Nullable SealedSenderAccess sealedSenderAccess, int deviceId, boolean story) throws IOException { try { // If it's only unrestricted because it's a story send, then we know it'll fail