Handle duplicate device ids more gracefully

This commit is contained in:
Ravi Khadiwala
2022-07-27 10:01:43 -05:00
committed by ravi-signal
parent 98760b631b
commit 36050f580e
4 changed files with 117 additions and 60 deletions

View File

@@ -5,11 +5,14 @@
package org.whispersystems.textsecuregcm.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.controllers.StaleDevicesException;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -17,39 +20,50 @@ import org.whispersystems.textsecuregcm.storage.Device;
public class DestinationDeviceValidator {
/**
* @see #validateRegistrationIds(Account, Stream, boolean)
*/
public static <T> void validateRegistrationIds(final Account account, final Collection<T> messages,
Function<T, Long> getDeviceId, Function<T, Integer> getRegistrationId, boolean usePhoneNumberIdentity)
throws StaleDevicesException {
validateRegistrationIds(account,
messages.stream().map(m -> new Pair<>(getDeviceId.apply(m), getRegistrationId.apply(m))),
usePhoneNumberIdentity);
}
/**
* Validates that the given device ID/registration ID pairs exactly match the corresponding device ID/registration ID
* pairs in the given destination account. This method does <em>not</em> validate that all devices associated with the
* destination account are present in the given device ID/registration ID pairs.
*
* @param account the destination account against which to check the given device ID/registration ID pairs
* @param registrationIdsByDeviceId a map of device IDs to registration IDs
* @param usePhoneNumberIdentity if {@code true}, compare provided registration IDs against device registration IDs
* associated with the account's PNI (if available); compare against the ACI-associated
* registration ID otherwise
*
* @param account the destination account against which to check the given device
* ID/registration ID pairs
* @param deviceIdAndRegistrationIdStream a stream of device ID and registration ID pairs
* @param usePhoneNumberIdentity if {@code true}, compare provided registration IDs against device
* registration IDs associated with the account's PNI (if available); compare
* against the ACI-associated registration ID otherwise
* @throws StaleDevicesException if the device ID/registration ID pairs contained an entry for which the destination
* account does not have a corresponding device or if the registration IDs do not match
*/
public static void validateRegistrationIds(final Account account,
final Map<Long, Integer> registrationIdsByDeviceId,
final Stream<Pair<Long, Integer>> deviceIdAndRegistrationIdStream,
final boolean usePhoneNumberIdentity) throws StaleDevicesException {
final List<Long> staleDevices = new ArrayList<>();
registrationIdsByDeviceId.forEach((deviceId, registrationId) -> {
if (registrationId > 0) {
final boolean registrationIdMatches =
account.getDevice(deviceId).map(device -> registrationId == (usePhoneNumberIdentity ?
device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId()) :
device.getRegistrationId()))
.orElse(false);
if (!registrationIdMatches) {
staleDevices.add(deviceId);
}
}
});
final List<Long> staleDevices = deviceIdAndRegistrationIdStream
.filter(deviceIdAndRegistrationId -> deviceIdAndRegistrationId.second() > 0)
.filter(deviceIdAndRegistrationId -> {
final long deviceId = deviceIdAndRegistrationId.first();
final int registrationId = deviceIdAndRegistrationId.second();
boolean registrationIdMatches = account.getDevice(deviceId)
.map(device -> registrationId == (usePhoneNumberIdentity
? device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId())
: device.getRegistrationId()))
.orElse(false);
return !registrationIdMatches;
})
.map(Pair::first)
.collect(Collectors.toList());
if (!staleDevices.isEmpty()) {
throw new StaleDevicesException(staleDevices);
@@ -63,11 +77,10 @@ public class DestinationDeviceValidator {
* "sync," message, though, the authenticated account is sending messages from one of their devices to all other
* devices; in that case, callers must pass the ID of the sending device in the set of {@code excludedDeviceIds}.
*
* @param account the destination account against which to check the given set of device IDs
* @param messageDeviceIds the set of device IDs to check against the destination account
* @param account the destination account against which to check the given set of device IDs
* @param messageDeviceIds the set of device IDs to check against the destination account
* @param excludedDeviceIds a set of device IDs that may be associated with the destination account, but must not be
* present in the given set of device IDs (i.e. the device that is sending a sync message)
*
* @throws MismatchedDevicesException if the given set of device IDs contains entries not currently associated with
* the destination account or is missing entries associated with the destination
* account