diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/DeviceLastSeenState.java b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/DeviceLastSeenState.java index f57d16f60..c1750f003 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/DeviceLastSeenState.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/DeviceLastSeenState.java @@ -3,7 +3,11 @@ package org.whispersystems.textsecuregcm.experiment; import javax.annotation.Nullable; public record DeviceLastSeenState(boolean deviceExists, - long createdAtMillis, + // Registration IDs are not guaranteed to be unique across devices and re-registrations. + // However, for this use case, we accept the possibility of collisions in order to + // avoid storing plaintext device creation timestamps on the server. + // This tradeoff is intentional and aligned with our privacy goals. + int registrationId, boolean hasPushToken, long lastSeenMillis, @Nullable PushTokenType pushTokenType) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperiment.java b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperiment.java index fc3779d29..c44aea9b5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperiment.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperiment.java @@ -4,6 +4,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.workers.IdleWakeupEligibilityChecker; @@ -64,7 +65,7 @@ abstract class IdleDevicePushNotificationExperiment implements PushNotificationE } else { pushTokenType = null; } - return new DeviceLastSeenState(true, device.getCreated(), hasPushToken(device), device.getLastSeen(), pushTokenType); + return new DeviceLastSeenState(true, device.getRegistrationId(IdentityType.ACI), hasPushToken(device), device.getLastSeen(), pushTokenType); } else { return DeviceLastSeenState.MISSING_DEVICE_STATE; } @@ -116,7 +117,7 @@ abstract class IdleDevicePushNotificationExperiment implements PushNotificationE assert sample.finalState() != null; - if (!sample.finalState().deviceExists() || sample.initialState().createdAtMillis() != sample.finalState().createdAtMillis()) { + if (!sample.finalState().deviceExists() || sample.initialState().registrationId() != sample.finalState().registrationId()) { outcome = Outcome.DELETED; } else if (!sample.finalState().hasPushToken()) { outcome = Outcome.UNINSTALLED; diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperimentTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperimentTest.java index cfc6bdfde..90a899f31 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperimentTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/experiment/IdleDevicePushNotificationExperimentTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; import reactor.core.publisher.Flux; @@ -66,37 +67,37 @@ abstract class IdleDevicePushNotificationExperimentTest { assertEquals(DeviceLastSeenState.MISSING_DEVICE_STATE, experiment.getState(null, null)); assertEquals(DeviceLastSeenState.MISSING_DEVICE_STATE, experiment.getState(mock(Account.class), null)); - final long createdAtMillis = CURRENT_TIME.minus(Duration.ofDays(14)).toEpochMilli(); + final int registrationId = 123; { final Device apnsDevice = mock(Device.class); when(apnsDevice.getApnId()).thenReturn("apns-token"); - when(apnsDevice.getCreated()).thenReturn(createdAtMillis); + when(apnsDevice.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId); when(apnsDevice.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); assertEquals( - new DeviceLastSeenState(true, createdAtMillis, true, CURRENT_TIME.toEpochMilli(), DeviceLastSeenState.PushTokenType.APNS), + new DeviceLastSeenState(true, registrationId, true, CURRENT_TIME.toEpochMilli(), DeviceLastSeenState.PushTokenType.APNS), experiment.getState(mock(Account.class), apnsDevice)); } { final Device fcmDevice = mock(Device.class); when(fcmDevice.getGcmId()).thenReturn("fcm-token"); - when(fcmDevice.getCreated()).thenReturn(createdAtMillis); + when(fcmDevice.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId); when(fcmDevice.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); assertEquals( - new DeviceLastSeenState(true, createdAtMillis, true, CURRENT_TIME.toEpochMilli(), DeviceLastSeenState.PushTokenType.FCM), + new DeviceLastSeenState(true, registrationId, true, CURRENT_TIME.toEpochMilli(), DeviceLastSeenState.PushTokenType.FCM), experiment.getState(mock(Account.class), fcmDevice)); } { final Device noTokenDevice = mock(Device.class); - when(noTokenDevice.getCreated()).thenReturn(createdAtMillis); + when(noTokenDevice.getRegistrationId(IdentityType.ACI)).thenReturn(registrationId); when(noTokenDevice.getLastSeen()).thenReturn(CURRENT_TIME.toEpochMilli()); assertEquals( - new DeviceLastSeenState(true, createdAtMillis, false, CURRENT_TIME.toEpochMilli(), null), + new DeviceLastSeenState(true, registrationId, false, CURRENT_TIME.toEpochMilli(), null), experiment.getState(mock(Account.class), noTokenDevice)); } } @@ -150,10 +151,10 @@ abstract class IdleDevicePushNotificationExperimentTest { IdleDevicePushNotificationExperiment.Outcome.DELETED ), - // Device re-registered (i.e. "created" timestamp changed) + // Device re-registered (i.e. registration ID changed) Arguments.of( - new DeviceLastSeenState(true, 0, true, 0, DeviceLastSeenState.PushTokenType.APNS), - new DeviceLastSeenState(true, 1, true, 1, DeviceLastSeenState.PushTokenType.APNS), + new DeviceLastSeenState(true, 123, true, 0, DeviceLastSeenState.PushTokenType.APNS), + new DeviceLastSeenState(true, 1234, true, 1, DeviceLastSeenState.PushTokenType.APNS), IdleDevicePushNotificationExperiment.Outcome.DELETED ),