Skip to content

Commit db4c713

Browse files
Use registration ID or creation timestamp in the transfer archive flow
1 parent 30774bb commit db4c713

File tree

6 files changed

+309
-88
lines changed

6 files changed

+309
-88
lines changed

service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ public class DeviceController {
115115
private static final String WAIT_FOR_TRANSFER_ARCHIVE_TIMER_NAME =
116116
MetricsUtil.name(DeviceController.class, "waitForTransferArchiveDuration");
117117

118+
private static final String RECORD_TRANSFER_ARCHIVE_UPLOADED_COUNTER_NAME = MetricsUtil.name(DeviceController.class, "recordTransferArchiveUploaded");
119+
private static final String HAS_REGISTRATION_ID_TAG_NAME = "hasRegistrationId";
118120

119121
@VisibleForTesting
120122
static final int MIN_TOKEN_IDENTIFIER_LENGTH = 32;
@@ -533,8 +535,14 @@ The amount of time (in seconds) to wait for a response. If a transfer archive fo
533535
@ApiResponse(responseCode = "422", description = "The request object could not be parsed or was otherwise invalid")
534536
@ApiResponse(responseCode = "429", description = "Rate-limited; try again after the prescribed delay")
535537
public CompletionStage<Void> recordTransferArchiveUploaded(@Auth final AuthenticatedDevice authenticatedDevice,
536-
@NotNull @Valid final TransferArchiveUploadedRequest transferArchiveUploadedRequest) {
537-
538+
@NotNull @Valid final TransferArchiveUploadedRequest transferArchiveUploadedRequest,
539+
@HeaderParam(HttpHeaders.USER_AGENT) @Nullable String userAgent) {
540+
Metrics.counter(RECORD_TRANSFER_ARCHIVE_UPLOADED_COUNTER_NAME, Tags.of(
541+
UserAgentTagUtil.getPlatformTag(userAgent),
542+
io.micrometer.core.instrument.Tag.of(
543+
HAS_REGISTRATION_ID_TAG_NAME,
544+
String.valueOf(transferArchiveUploadedRequest.registrationId().isPresent()))
545+
)).increment();
538546
return rateLimiters.getUploadTransferArchiveLimiter()
539547
.validateAsync(authenticatedDevice.accountIdentifier())
540548
.thenCompose(ignored -> accounts.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
@@ -544,7 +552,8 @@ public CompletionStage<Void> recordTransferArchiveUploaded(@Auth final Authentic
544552

545553
return accounts.recordTransferArchiveUpload(account,
546554
transferArchiveUploadedRequest.destinationDeviceId(),
547-
Instant.ofEpochMilli(transferArchiveUploadedRequest.destinationDeviceCreated()),
555+
transferArchiveUploadedRequest.destinationDeviceCreated().map(Instant::ofEpochMilli),
556+
transferArchiveUploadedRequest.registrationId(),
548557
transferArchiveUploadedRequest.transferArchive());
549558
});
550559
}

service/src/main/java/org/whispersystems/textsecuregcm/entities/TransferArchiveUploadedRequest.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,41 @@
77

88
import io.swagger.v3.oas.annotations.media.Schema;
99
import jakarta.validation.Valid;
10+
import jakarta.validation.constraints.AssertTrue;
1011
import jakarta.validation.constraints.Max;
1112
import jakarta.validation.constraints.Min;
1213
import jakarta.validation.constraints.NotNull;
1314
import jakarta.validation.constraints.Positive;
1415
import org.whispersystems.textsecuregcm.storage.Device;
1516

17+
import java.util.Optional;
18+
1619
public record TransferArchiveUploadedRequest(
1720
@Min(1)
1821
@Max(Device.MAXIMUM_DEVICE_ID)
1922
@Schema(description = "The ID of the device for which the transfer archive has been prepared")
2023
byte destinationDeviceId,
2124

22-
@Positive
23-
@Schema(description = "The timestamp, in milliseconds since the epoch, at which the destination device was created")
24-
long destinationDeviceCreated,
25+
@Schema(description = """
26+
The timestamp, in milliseconds since the epoch, at which the destination device was created.
27+
Deprecated in favor of registrationId.
28+
""", deprecated = true)
29+
@Deprecated
30+
Optional<@Positive Long> destinationDeviceCreated,
31+
32+
@Schema(description = "The registration ID of the destination device")
33+
Optional<@Min(0) @Max(Device.MAX_REGISTRATION_ID) Integer> registrationId,
2534

2635
@NotNull
2736
@Valid
2837
@Schema(description = """
2938
The location of the transfer archive if the archive was successfully uploaded, otherwise a error indicating that
3039
the upload has failed and the destination device should stop waiting
3140
""", oneOf = {RemoteAttachment.class, RemoteAttachmentError.class})
32-
TransferArchiveResult transferArchive) {}
41+
TransferArchiveResult transferArchive) {
42+
@AssertTrue
43+
@Schema(hidden = true)
44+
public boolean isExactlyOneDisambiguatorProvided() {
45+
return destinationDeviceCreated.isPresent() ^ registrationId.isPresent();
46+
}
47+
}

service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java

Lines changed: 101 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.concurrent.Executor;
5151
import java.util.concurrent.ScheduledExecutorService;
5252
import java.util.concurrent.TimeUnit;
53+
import java.util.concurrent.atomic.AtomicInteger;
5354
import java.util.concurrent.atomic.AtomicReference;
5455
import java.util.function.BiConsumer;
5556
import java.util.function.Consumer;
@@ -85,6 +86,7 @@
8586
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
8687
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
8788
import org.whispersystems.textsecuregcm.util.Pair;
89+
import org.whispersystems.textsecuregcm.util.RegistrationIdValidator;
8890
import org.whispersystems.textsecuregcm.util.SystemMapper;
8991
import org.whispersystems.textsecuregcm.util.Util;
9092
import reactor.core.publisher.Flux;
@@ -111,6 +113,8 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
111113
private static final String DELETE_COUNTER_NAME = name(AccountsManager.class, "deleteCounter");
112114
private static final String COUNTRY_CODE_TAG_NAME = "country";
113115
private static final String DELETION_REASON_TAG_NAME = "reason";
116+
private static final String TIMESTAMP_BASED_TRANSFER_ARCHIVE_KEY_COUNTER_NAME = name(AccountsManager.class, "timestampRedisKeyCounter");
117+
private static final String REGISTRATION_ID_BASED_TRANSFER_ARCHIVE_KEY_COUNTER_NAME = name(AccountsManager.class,"registrationIdRedisKeyCounter");
114118

115119
private static final Logger logger = LoggerFactory.getLogger(AccountsManager.class);
116120

@@ -140,7 +144,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
140144
private final Map<String, CompletableFuture<Optional<DeviceInfo>>> waitForDeviceFuturesByTokenIdentifier =
141145
new ConcurrentHashMap<>();
142146

143-
private final Map<TimestampedDeviceIdentifier, CompletableFuture<Optional<TransferArchiveResult>>> waitForTransferArchiveFuturesByDeviceIdentifier =
147+
private final Map<DeviceIdentifier, CompletableFuture<Optional<TransferArchiveResult>>> waitForTransferArchiveFuturesByDeviceIdentifier =
144148
new ConcurrentHashMap<>();
145149

146150
private final Map<String, CompletableFuture<Optional<RestoreAccountRequest>>> waitForRestoreAccountRequestFuturesByToken =
@@ -155,6 +159,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
155159
private static final Duration RECENTLY_ADDED_TRANSFER_ARCHIVE_TTL = Duration.ofHours(1);
156160
private static final String TRANSFER_ARCHIVE_PREFIX = "transfer_archive::";
157161
private static final String TRANSFER_ARCHIVE_KEYSPACE_PATTERN = "__keyspace@0__:" + TRANSFER_ARCHIVE_PREFIX + "*";
162+
private static final String TRANSFER_ARCHIVE_REGISTRATION_ID_PATTERN = "registrationId";
158163

159164
private static final Duration RESTORE_ACCOUNT_REQUEST_TTL = Duration.ofHours(1);
160165
private static final String RESTORE_ACCOUNT_REQUEST_PREFIX = "restore_account::";
@@ -194,7 +199,14 @@ public enum DeletionReason {
194199
}
195200
}
196201

197-
private record TimestampedDeviceIdentifier(UUID accountIdentifier, byte deviceId, Instant deviceCreationTimestamp) {
202+
private interface DeviceIdentifier {}
203+
204+
private record TimestampDeviceIdentifier(UUID accountIdentifier, byte deviceId, Instant deviceCreationTimestamp)
205+
implements DeviceIdentifier {
206+
}
207+
208+
private record RegistrationIdDeviceIdentifier(UUID accountIdentifier, byte deviceId,
209+
int registrationId) implements DeviceIdentifier {
198210
}
199211

200212
public AccountsManager(final Accounts accounts,
@@ -1509,34 +1521,66 @@ private static String getLinkedDeviceKey(final String linkDeviceTokenIdentifier)
15091521
}
15101522

15111523
public CompletableFuture<Optional<TransferArchiveResult>> waitForTransferArchive(final Account account, final Device device, final Duration timeout) {
1512-
final TimestampedDeviceIdentifier deviceIdentifier =
1513-
new TimestampedDeviceIdentifier(account.getIdentifier(IdentityType.ACI),
1514-
device.getId(),
1515-
Instant.ofEpochMilli(device.getCreated()));
1516-
1517-
return waitForPubSubKey(waitForTransferArchiveFuturesByDeviceIdentifier,
1518-
deviceIdentifier,
1519-
getTransferArchiveKey(account.getIdentifier(IdentityType.ACI), device.getId(), Instant.ofEpochMilli(device.getCreated())),
1524+
final DeviceIdentifier timestampDeviceIdentifier = new TimestampDeviceIdentifier(account.getIdentifier(IdentityType.ACI), device.getId(), Instant.ofEpochMilli(device.getCreated()));
1525+
final String timestampTransferArchiveKey = getTimestampTransferArchiveKey(account.getIdentifier(IdentityType.ACI), device.getId(), Instant.ofEpochMilli(device.getCreated()));
1526+
1527+
final DeviceIdentifier registrationIdDeviceIdentifier = new RegistrationIdDeviceIdentifier(account.getIdentifier(IdentityType.ACI), device.getId(), device.getRegistrationId(IdentityType.ACI));
1528+
final String registrationIdTransferArchiveKey = getRegistrationIdTransferArchiveKey(account.getIdentifier(IdentityType.ACI), device.getId(), device.getRegistrationId(IdentityType.ACI));
1529+
1530+
final CompletableFuture<Optional<TransferArchiveResult>> timestampFuture = waitForPubSubKey(waitForTransferArchiveFuturesByDeviceIdentifier,
1531+
timestampDeviceIdentifier,
1532+
timestampTransferArchiveKey,
15201533
timeout,
15211534
this::handleTransferArchiveAdded);
1535+
1536+
final CompletableFuture<Optional<TransferArchiveResult>> registrationIdFuture = waitForPubSubKey(waitForTransferArchiveFuturesByDeviceIdentifier,
1537+
registrationIdDeviceIdentifier,
1538+
registrationIdTransferArchiveKey,
1539+
timeout,
1540+
this::handleTransferArchiveAdded);
1541+
return firstSuccessfulTransferArchiveFuture(List.of(timestampFuture, registrationIdFuture));
1542+
}
1543+
1544+
@VisibleForTesting
1545+
static CompletableFuture<Optional<TransferArchiveResult>> firstSuccessfulTransferArchiveFuture(
1546+
final List<CompletableFuture<Optional<TransferArchiveResult>>> futures) {
1547+
final CompletableFuture<Optional<TransferArchiveResult>> result = new CompletableFuture<>();
1548+
final AtomicInteger remaining = new AtomicInteger(futures.size());
1549+
1550+
for (CompletableFuture<Optional<TransferArchiveResult>> future : futures) {
1551+
future.whenComplete((value, _) -> {
1552+
if (value.isPresent()) {
1553+
result.complete(value);
1554+
} else if (remaining.decrementAndGet() == 0) {
1555+
result.complete(Optional.empty());
1556+
}
1557+
});
1558+
}
1559+
1560+
return result;
15221561
}
15231562

15241563
public CompletableFuture<Void> recordTransferArchiveUpload(final Account account,
15251564
final byte destinationDeviceId,
1526-
final Instant destinationDeviceCreationTimestamp,
1565+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Instant> destinationDeviceCreationTimestamp,
1566+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Integer> maybeRegistrationId,
15271567
final TransferArchiveResult transferArchiveResult) {
1528-
1529-
final String key = getTransferArchiveKey(account.getIdentifier(IdentityType.ACI),
1530-
destinationDeviceId,
1531-
destinationDeviceCreationTimestamp);
1532-
15331568
try {
15341569
final String transferArchiveJson = SystemMapper.jsonMapper().writeValueAsString(transferArchiveResult);
15351570

1536-
return pubSubRedisClient.withConnection(connection ->
1537-
connection.async().set(key, transferArchiveJson, SetArgs.Builder.ex(RECENTLY_ADDED_TRANSFER_ARCHIVE_TTL)))
1538-
.thenRun(Util.NOOP)
1539-
.toCompletableFuture();
1571+
return pubSubRedisClient.withConnection(connection -> {
1572+
final String key = destinationDeviceCreationTimestamp
1573+
.map(timestamp -> getTimestampTransferArchiveKey(account.getIdentifier(IdentityType.ACI), destinationDeviceId, timestamp))
1574+
.orElseGet(() -> maybeRegistrationId
1575+
.map(registrationId -> getRegistrationIdTransferArchiveKey(account.getIdentifier(IdentityType.ACI), destinationDeviceId, registrationId))
1576+
// We validate the request object so this should never happen
1577+
.orElseThrow(() -> new AssertionError("No creation timestamp or registration ID provided")));
1578+
1579+
return connection.async()
1580+
.set(key, transferArchiveJson, SetArgs.Builder.ex(RECENTLY_ADDED_TRANSFER_ARCHIVE_TTL))
1581+
.thenRun(Util.NOOP)
1582+
.toCompletableFuture();
1583+
});
15401584
} catch (final JsonProcessingException e) {
15411585
// This should never happen for well-defined objects we control
15421586
throw new UncheckedIOException(e);
@@ -1552,15 +1596,27 @@ private void handleTransferArchiveAdded(final CompletableFuture<Optional<Transfe
15521596
}
15531597
}
15541598

1555-
private static String getTransferArchiveKey(final UUID accountIdentifier,
1599+
private static String getTimestampTransferArchiveKey(final UUID accountIdentifier,
15561600
final byte destinationDeviceId,
15571601
final Instant destinationDeviceCreationTimestamp) {
1602+
Metrics.counter(TIMESTAMP_BASED_TRANSFER_ARCHIVE_KEY_COUNTER_NAME).increment();
15581603

15591604
return TRANSFER_ARCHIVE_PREFIX + accountIdentifier.toString() +
15601605
":" + destinationDeviceId +
15611606
":" + destinationDeviceCreationTimestamp.toEpochMilli();
15621607
}
15631608

1609+
private static String getRegistrationIdTransferArchiveKey(final UUID accountIdentifier,
1610+
final byte destinationDeviceId,
1611+
final int registrationId) {
1612+
Metrics.counter(REGISTRATION_ID_BASED_TRANSFER_ARCHIVE_KEY_COUNTER_NAME).increment();
1613+
1614+
return TRANSFER_ARCHIVE_PREFIX + accountIdentifier.toString() +
1615+
":" + destinationDeviceId +
1616+
":" + TRANSFER_ARCHIVE_REGISTRATION_ID_PATTERN +
1617+
":" + registrationId;
1618+
}
1619+
15641620
public CompletableFuture<Optional<RestoreAccountRequest>> waitForRestoreAccountRequest(final String token, final Duration timeout) {
15651621
return waitForPubSubKey(waitForRestoreAccountRequestFuturesByToken,
15661622
token,
@@ -1648,23 +1704,36 @@ public void message(final String pattern, final String channel, final String mes
16481704
} else if (TRANSFER_ARCHIVE_KEYSPACE_PATTERN.equals(pattern) && "set".equalsIgnoreCase(message)) {
16491705
// The `- 1` here compensates for the '*' in the pattern
16501706
final String[] deviceIdentifierComponents =
1651-
channel.substring(TRANSFER_ARCHIVE_KEYSPACE_PATTERN.length() - 1).split(":", 3);
1707+
channel.substring(TRANSFER_ARCHIVE_KEYSPACE_PATTERN.length() - 1).split(":", 4);
16521708

1653-
if (deviceIdentifierComponents.length != 3) {
1654-
logger.error("Could not parse timestamped device identifier; unexpected component count");
1709+
if (deviceIdentifierComponents.length != 3 && deviceIdentifierComponents.length != 4) {
1710+
logger.error("Could not parse device identifier; unexpected component count");
16551711
return;
16561712
}
16571713

1714+
final DeviceIdentifier deviceIdentifier;
1715+
final String transferArchiveKey;
16581716
try {
1659-
final TimestampedDeviceIdentifier deviceIdentifier;
1660-
final String transferArchiveKey;
1661-
{
1662-
final UUID accountIdentifier = UUID.fromString(deviceIdentifierComponents[0]);
1663-
final byte deviceId = Byte.parseByte(deviceIdentifierComponents[1]);
1717+
final UUID accountIdentifier = UUID.fromString(deviceIdentifierComponents[0]);
1718+
final byte deviceId = Byte.parseByte(deviceIdentifierComponents[1]);
1719+
1720+
if (deviceIdentifierComponents.length == 3) {
1721+
// Parse the old transfer archive Redis key format
16641722
final Instant deviceCreationTimestamp = Instant.ofEpochMilli(Long.parseLong(deviceIdentifierComponents[2]));
16651723

1666-
deviceIdentifier = new TimestampedDeviceIdentifier(accountIdentifier, deviceId, deviceCreationTimestamp);
1667-
transferArchiveKey = getTransferArchiveKey(accountIdentifier, deviceId, deviceCreationTimestamp);
1724+
deviceIdentifier = new TimestampDeviceIdentifier(accountIdentifier, deviceId, deviceCreationTimestamp);
1725+
transferArchiveKey = getTimestampTransferArchiveKey(accountIdentifier, deviceId, deviceCreationTimestamp);
1726+
} else {
1727+
final String maybeRegistrationIdPattern = deviceIdentifierComponents[2];
1728+
if (!maybeRegistrationIdPattern.equals(TRANSFER_ARCHIVE_REGISTRATION_ID_PATTERN)) {
1729+
throw new IllegalArgumentException("Could not parse Redis key with pattern " + maybeRegistrationIdPattern);
1730+
}
1731+
final int registrationId = Integer.parseInt(deviceIdentifierComponents[3]);
1732+
if (!RegistrationIdValidator.validRegistrationId(registrationId)) {
1733+
throw new IllegalArgumentException("Invalid registration ID: " + registrationId);
1734+
}
1735+
deviceIdentifier = new RegistrationIdDeviceIdentifier(accountIdentifier, deviceId, registrationId);
1736+
transferArchiveKey = getRegistrationIdTransferArchiveKey(accountIdentifier, deviceId, registrationId);
16681737
}
16691738

16701739
Optional.ofNullable(waitForTransferArchiveFuturesByDeviceIdentifier.remove(deviceIdentifier))
@@ -1677,7 +1746,7 @@ public void message(final String pattern, final String channel, final String mes
16771746
}
16781747
}));
16791748
} catch (final IllegalArgumentException e) {
1680-
logger.error("Could not parse timestamped device identifier", e);
1749+
logger.error("Could not parse device identifier", e);
16811750
}
16821751
} else if (RESTORE_ACCOUNT_REQUEST_KEYSPACE_PATTERN.equalsIgnoreCase(pattern) && "set".equalsIgnoreCase(message)) {
16831752
// The `- 1` here compensates for the '*' in the pattern

0 commit comments

Comments
 (0)