diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 773f411962..a7cc883ab4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -421,12 +421,15 @@ public final class PushGroupSendJob extends PushSendJob { List successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList(); List> successUnidentifiedStatus = Stream.of(successes).map(result -> new Pair<>(accessList.requireIdByAddress(result.getAddress()), result.getSuccess().isUnidentified())).toList(); Set successIds = Stream.of(successUnidentifiedStatus).map(Pair::first).collect(Collectors.toSet()); - List resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList(); - List resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList(); + Set resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successIds.contains(failure.getRecipientId(context))).collect(Collectors.toSet()); + Set resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successIds.contains(failure.getRecipientId(context))).collect(Collectors.toSet()); List unregisteredRecipients = Stream.of(results).filter(SendMessageResult::isUnregisteredFailure).map(result -> RecipientId.from(result.getAddress())).toList(); - List skippedRecipients = new ArrayList<>(unregisteredRecipients); + List invalidPreKeyRecipients = Stream.of(results).filter(SendMessageResult::isInvalidPreKeyFailure).map(result -> RecipientId.from(result.getAddress())).toList(); + Set skippedRecipients = new HashSet<>(); skippedRecipients.addAll(skipped); + skippedRecipients.addAll(unregisteredRecipients); + skippedRecipients.addAll(invalidPreKeyRecipients); if (networkFailures.size() > 0 || identityMismatches.size() > 0 || proofRequired != null || unregisteredRecipients.size() > 0) { Log.w(TAG, String.format(Locale.US, "Failed to send to some recipients. Network: %d, Identity: %d, ProofRequired: %s, Unregistered: %d", @@ -439,10 +442,12 @@ public final class PushGroupSendJob extends PushSendJob { } existingNetworkFailures.removeAll(resolvedNetworkFailures); + existingNetworkFailures.removeIf(it -> skippedRecipients.contains(it.getRecipientId(context))); existingNetworkFailures.addAll(networkFailures); database.setNetworkFailures(messageId, existingNetworkFailures); existingIdentityMismatches.removeAll(resolvedIdentityFailures); + existingIdentityMismatches.removeIf(it -> skippedRecipients.contains(it.getRecipientId(context))); existingIdentityMismatches.addAll(identityMismatches); database.setMismatchedIdentities(messageId, existingIdentityMismatches); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/InvalidPreKeyException.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/InvalidPreKeyException.kt new file mode 100644 index 0000000000..ff648aa5bc --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/InvalidPreKeyException.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api + +import org.signal.libsignal.protocol.InvalidKeyException +import org.signal.libsignal.protocol.SignalProtocolAddress +import java.io.IOException + +/** + * Wraps an [InvalidKeyException] in an [IOException] with a nicer message. + */ +class InvalidPreKeyException( + address: SignalProtocolAddress, + invalidKeyException: InvalidKeyException +) : IOException("Invalid prekey for $address", invalidKeyException) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index ceef56ac6c..2c565d7747 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -1849,24 +1849,28 @@ public class SignalServiceMessageSender { results.add(futureResult.get()); } catch (ExecutionException e) { if (e.getCause() instanceof UntrustedIdentityException) { - Log.w(TAG, e); + Log.w(TAG, "[" + timestamp + "] Hit identity mismatch: " + recipient.getIdentifier(), e); results.add(SendMessageResult.identityFailure(recipient, ((UntrustedIdentityException) e.getCause()).getIdentityKey())); } else if (e.getCause() instanceof UnregisteredUserException) { - Log.w(TAG, "[" + timestamp + "] Found unregistered user."); + Log.w(TAG, "[" + timestamp + "] Hit unregistered user: " + recipient.getIdentifier()); results.add(SendMessageResult.unregisteredFailure(recipient)); } else if (e.getCause() instanceof PushNetworkException) { - Log.w(TAG, e); + Log.w(TAG, "[" + timestamp + "] Hit network failure: " + recipient.getIdentifier(), e); results.add(SendMessageResult.networkFailure(recipient)); } else if (e.getCause() instanceof ServerRejectedException) { - Log.w(TAG, e); + Log.w(TAG, "[" + timestamp + "] Hit server rejection: " + recipient.getIdentifier(), e); throw ((ServerRejectedException) e.getCause()); } else if (e.getCause() instanceof ProofRequiredException) { - Log.w(TAG, e); + Log.w(TAG, "[" + timestamp + "] Hit proof required: " + recipient.getIdentifier(), e); results.add(SendMessageResult.proofRequiredFailure(recipient, (ProofRequiredException) e.getCause())); } else if (e.getCause() instanceof RateLimitException) { - Log.w(TAG, e); + Log.w(TAG, "[" + timestamp + "] Hit rate limit: " + recipient.getIdentifier(), e); results.add(SendMessageResult.rateLimitFailure(recipient, (RateLimitException) e.getCause())); + } else if (e.getCause() instanceof InvalidPreKeyException) { + Log.w(TAG, "[" + timestamp + "] Hit invalid prekey: " + recipient.getIdentifier(), e); + results.add(SendMessageResult.invalidPreKeyFailure(recipient)); } else { + Log.w(TAG, "[" + timestamp + "] Hit unknown exception: " + recipient.getIdentifier(), e); throw new IOException(e); } } catch (InterruptedException e) { @@ -2317,10 +2321,10 @@ public class SignalServiceMessageSender { // Visible for testing only public OutgoingPushMessage getEncryptedMessage(SignalServiceAddress recipient, - Optional unidentifiedAccess, - int deviceId, - EnvelopeContent plaintext, - boolean story) + Optional unidentifiedAccess, + int deviceId, + EnvelopeContent plaintext, + boolean story) throws IOException, InvalidKeyException, UntrustedIdentityException { SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId); @@ -2331,6 +2335,8 @@ public class SignalServiceMessageSender { List preKeys = getPreKeys(recipient, unidentifiedAccess, deviceId, story); for (PreKeyBundle preKey : preKeys) { + Log.d(TAG, "Initializing prekey session for " + signalProtocolAddress); + try { SignalProtocolAddress preKeyAddress = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId()); SignalSessionBuilder sessionBuilder = new SignalSessionBuilder(sessionLock, new SessionBuilder(aciStore, preKeyAddress)); @@ -2344,7 +2350,7 @@ public class SignalServiceMessageSender { eventListener.get().onSecurityEvent(recipient); } } catch (InvalidKeyException e) { - throw new IOException(e); + throw new InvalidPreKeyException(signalProtocolAddress, e); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java index e7f8f5222d..a837b06bfe 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SendMessageResult.java @@ -19,29 +19,34 @@ public class SendMessageResult { private final IdentityFailure identityFailure; private final ProofRequiredException proofRequiredFailure; private final RateLimitException rateLimitFailure; + private final boolean invalidPreKeyFailure; public static SendMessageResult success(SignalServiceAddress address, List devices, boolean unidentified, boolean needsSync, long duration, Optional content) { - return new SendMessageResult(address, new Success(unidentified, needsSync, duration, content, devices), false, false, null, null, null); + return new SendMessageResult(address, new Success(unidentified, needsSync, duration, content, devices), false, false, null, null, null, false); } public static SendMessageResult networkFailure(SignalServiceAddress address) { - return new SendMessageResult(address, null, true, false, null, null, null); + return new SendMessageResult(address, null, true, false, null, null, null, false); } public static SendMessageResult unregisteredFailure(SignalServiceAddress address) { - return new SendMessageResult(address, null, false, true, null, null, null); + return new SendMessageResult(address, null, false, true, null, null, null, false); } public static SendMessageResult identityFailure(SignalServiceAddress address, IdentityKey identityKey) { - return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey), null, null); + return new SendMessageResult(address, null, false, false, new IdentityFailure(identityKey), null, null, false); } public static SendMessageResult proofRequiredFailure(SignalServiceAddress address, ProofRequiredException proofRequiredException) { - return new SendMessageResult(address, null, false, false, null, proofRequiredException, null); + return new SendMessageResult(address, null, false, false, null, proofRequiredException, null, false); } public static SendMessageResult rateLimitFailure(SignalServiceAddress address, RateLimitException rateLimitException) { - return new SendMessageResult(address, null, false, false, null, null, rateLimitException); + return new SendMessageResult(address, null, false, false, null, null, rateLimitException, false); + } + + public static SendMessageResult invalidPreKeyFailure(SignalServiceAddress address) { + return new SendMessageResult(address, null, false, false, null, null, null, true); } public SignalServiceAddress getAddress() { @@ -76,13 +81,18 @@ public class SendMessageResult { return rateLimitFailure; } + public boolean isInvalidPreKeyFailure() { + return invalidPreKeyFailure; + } + private SendMessageResult(SignalServiceAddress address, Success success, boolean networkFailure, boolean unregisteredFailure, IdentityFailure identityFailure, ProofRequiredException proofRequiredFailure, - RateLimitException rateLimitFailure) + RateLimitException rateLimitFailure, + boolean invalidPreKeyFailure) { this.address = address; this.success = success; @@ -91,6 +101,7 @@ public class SendMessageResult { this.identityFailure = identityFailure; this.proofRequiredFailure = proofRequiredFailure; this.rateLimitFailure = rateLimitFailure; + this.invalidPreKeyFailure = invalidPreKeyFailure; } public static class Success {