Add versionedExpirationTimer capability

This commit is contained in:
Katherine
2024-08-21 11:39:43 -04:00
committed by GitHub
parent fa51793379
commit 4c0a5ac3b2
20 changed files with 139 additions and 37 deletions

View File

@@ -216,7 +216,7 @@ class DeviceControllerTest {
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null,
null, true, new DeviceCapabilities(true, true, true, false));
null, true, new DeviceCapabilities(true, true, true, false, false));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
accountAttributes,
@@ -293,7 +293,7 @@ class DeviceControllerTest {
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, true, deviceSupportsDeleteSync)),
new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, true, deviceSupportsDeleteSync, false)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
try (final Response response = resources.getJerseyTest()
@@ -314,6 +314,59 @@ class DeviceControllerTest {
Arguments.of(false, false, 200));
}
@ParameterizedTest
@MethodSource
void deviceDowngradeVersionedExpirationTimer(final boolean accountSupportsVersionedExpirationTimer,
final boolean deviceSupportsVersionedExpirationTimer, final int expectedStatus) {
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
when(accountsManager.addDevice(any(), any()))
.thenReturn(CompletableFuture.completedFuture(new Pair<>(mock(Account.class), mock(Device.class))));
final Device primaryDevice = mock(Device.class);
when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(primaryDevice));
final ECSignedPreKey aciSignedPreKey;
final ECSignedPreKey pniSignedPreKey;
final KEMSignedPreKey aciPqLastResortPreKey;
final KEMSignedPreKey pniPqLastResortPreKey;
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
when(account.isDeleteSyncSupported()).thenReturn(accountSupportsVersionedExpirationTimer);
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, true, deviceSupportsVersionedExpirationTimer, false)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
try (final Response response = resources.getJerseyTest()
.target("/v1/devices/link")
.request()
.header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
.put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
assertEquals(expectedStatus, response.getStatus());
}
}
private static List<Arguments> deviceDowngradeVersionedExpirationTimer() {
return List.of(
Arguments.of(true, true, 200),
Arguments.of(true, false, 409),
Arguments.of(false, true, 200),
Arguments.of(false, false, 200));
}
@Test
void linkDeviceAtomicBadCredentials() {
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
@@ -683,7 +736,7 @@ class DeviceControllerTest {
when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
new AccountAttributes(false, registrationId, pniRegistrationId, null, null, true, new DeviceCapabilities(true, true, true, false)),
new AccountAttributes(false, registrationId, pniRegistrationId, null, null, true, new DeviceCapabilities(true, true, true, false, false)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.of(new ApnRegistrationId("apn", null)), Optional.empty()));
try (final Response response = resources.getJerseyTest()
@@ -742,7 +795,7 @@ class DeviceControllerTest {
@Test
void putCapabilitiesSuccessTest() {
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false);
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false, false);
final Response response = resources
.getJerseyTest()
.target("/v1/devices/capabilities")

View File

@@ -59,6 +59,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.ArgumentCaptor;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ServiceId;
@@ -439,10 +440,12 @@ class ProfileControllerTest {
assertThat(response.getStatus()).isEqualTo(401);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testProfileCapabilities(final boolean isDeleteSyncSupported) {
@CartesianTest
void testProfileCapabilities(
@CartesianTest.Values(booleans = {true, false}) final boolean isDeleteSyncSupported,
@CartesianTest.Values(booleans = {true, false}) final boolean isVersionedExpirationTimerSupported) {
when(capabilitiesAccount.isDeleteSyncSupported()).thenReturn(isDeleteSyncSupported);
when(capabilitiesAccount.isVersionedExpirationTimerSupported()).thenReturn(isVersionedExpirationTimerSupported);
final BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID)
.request()
@@ -450,6 +453,7 @@ class ProfileControllerTest {
.get(BaseProfileResponse.class);
assertEquals(isDeleteSyncSupported, profile.getCapabilities().deleteSync());
assertEquals(isVersionedExpirationTimerSupported, profile.getCapabilities().versionedExpirationTimer());
assertThat(profile.getCapabilities().paymentActivation()).isTrue();
}

View File

@@ -526,10 +526,10 @@ class RegistrationControllerTest {
}
final AccountAttributes fetchesMessagesAccountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
final AccountAttributes pushAccountAttributes =
new AccountAttributes(false, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
new AccountAttributes(false, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
return Stream.of(
// "Fetches messages" is true, but an APNs token is provided
@@ -615,7 +615,7 @@ class RegistrationControllerTest {
}
final AccountAttributes accountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
return Stream.of(
// Signed PNI EC pre-key is missing
@@ -785,13 +785,13 @@ class RegistrationControllerTest {
final int registrationId = 1;
final int pniRegistrationId = 2;
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false, false);
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false, false, false);
final AccountAttributes fetchesMessagesAccountAttributes =
new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
final AccountAttributes pushAccountAttributes =
new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
final String apnsToken = "apns-token";
final String apnsVoipToken = "apns-voip-token";
@@ -906,7 +906,7 @@ class RegistrationControllerTest {
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
final AccountAttributes accountAttributes = new AccountAttributes(true, registrationId, pniRegistrationId, "name".getBytes(StandardCharsets.UTF_8), "reglock",
true, new Device.DeviceCapabilities(true, true, true, false));
true, new Device.DeviceCapabilities(true, true, true, false, false));
final RegistrationRequest request = new RegistrationRequest(
Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)),

View File

@@ -395,7 +395,8 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
@CartesianTest.Values(booleans = {true, false}) final boolean storage,
@CartesianTest.Values(booleans = {true, false}) final boolean transfer,
@CartesianTest.Values(booleans = {true, false}) final boolean paymentActivation,
@CartesianTest.Values(booleans = {true, false}) final boolean deleteSync) {
@CartesianTest.Values(booleans = {true, false}) final boolean deleteSync,
@CartesianTest.Values(booleans = {true, false}) final boolean versionedExpirationTimer) {
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId);
@@ -407,13 +408,15 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
.setTransfer(transfer)
.setPaymentActivation(paymentActivation)
.setDeleteSync(deleteSync)
.setVersionedExpirationTimer(versionedExpirationTimer)
.build());
final Device.DeviceCapabilities expectedCapabilities = new Device.DeviceCapabilities(
storage,
transfer,
paymentActivation,
deleteSync);
deleteSync,
versionedExpirationTimer);
verify(device).setCapabilities(expectedCapabilities);
}

View File

@@ -187,6 +187,7 @@ public class AccountCreationDeletionIntegrationTest {
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean());
final AccountAttributes accountAttributes = new AccountAttributes(deliveryChannels.fetchesMessages(),
@@ -297,14 +298,15 @@ public class AccountCreationDeletionIntegrationTest {
final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniKeyPair);
final Account originalAccount = accountsManager.create(number,
new AccountAttributes(true, 1, 1, "name".getBytes(StandardCharsets.UTF_8), "registration-lock", false, new Device.DeviceCapabilities(false, false, false, false)),
new AccountAttributes(true, 1, 1, "name".getBytes(StandardCharsets.UTF_8), "registration-lock", false,
new Device.DeviceCapabilities(false, false, false, false, false)),
Collections.emptyList(),
new IdentityKey(aciKeyPair.getPublicKey()),
new IdentityKey(pniKeyPair.getPublicKey()),
new DeviceSpec(null,
"password?",
"OWI",
new Device.DeviceCapabilities(false, false, false, false),
new Device.DeviceCapabilities(false, false, false, false, false),
1,
2,
true,
@@ -329,6 +331,7 @@ public class AccountCreationDeletionIntegrationTest {
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean());
final AccountAttributes accountAttributes = new AccountAttributes(deliveryChannels.fetchesMessages(),
@@ -419,6 +422,7 @@ public class AccountCreationDeletionIntegrationTest {
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean(),
ThreadLocalRandom.current().nextBoolean());
final AccountAttributes accountAttributes = new AccountAttributes(true,

View File

@@ -46,6 +46,8 @@ class AccountTest {
private final Device paymentActivationIncapableDeviceWithoutDeliveryChannel = mock(Device.class);
private final Device deleteSyncCapableDevice = mock(Device.class);
private final Device deleteSyncIncapableDevice = mock(Device.class);
private final Device versionedExpirationTimerCapableDevice = mock(Device.class);
private final Device versionedExpirationTimerIncapableDevice = mock(Device.class);
@BeforeEach
void setup() {
@@ -66,19 +68,27 @@ class AccountTest {
when(oldSecondaryDevice.getId()).thenReturn(deviceId2);
when(paymentActivationCapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, true, false));
.thenReturn(new DeviceCapabilities(true, true, true, false, false));
when(paymentActivationIncapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, false, false));
.thenReturn(new DeviceCapabilities(true, true, false, false, false));
when(paymentActivationIncapableDeviceWithoutDeliveryChannel.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, false, false));
.thenReturn(new DeviceCapabilities(true, true, false, false, false));
when(deleteSyncCapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, true, true));
.thenReturn(new DeviceCapabilities(true, true, true, true, false));
when(deleteSyncIncapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, true, false));
.thenReturn(new DeviceCapabilities(true, true, true, false, false));
when(versionedExpirationTimerCapableDevice.getId()).thenReturn((byte) 1);
when(versionedExpirationTimerCapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, true, false, true));
when(versionedExpirationTimerIncapableDevice.getId()).thenReturn((byte) 2);
when(versionedExpirationTimerIncapableDevice.getCapabilities())
.thenReturn(new DeviceCapabilities(true, true, true, false, false));
}
@@ -165,6 +175,16 @@ class AccountTest {
"1234".getBytes(StandardCharsets.UTF_8)).isDeleteSyncSupported());
}
@Test
void isVersionedExpirationTimerSupported() {
assertTrue(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
List.of(versionedExpirationTimerCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isVersionedExpirationTimerSupported());
assertFalse(AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
List.of(versionedExpirationTimerIncapableDevice, versionedExpirationTimerCapableDevice),
"1234".getBytes(StandardCharsets.UTF_8)).isVersionedExpirationTimerSupported());
}
@Test
void stale() {
final Account account = AccountsHelper.generateTestAccount("+14151234567", UUID.randomUUID(), UUID.randomUUID(), Collections.emptyList(),

View File

@@ -193,7 +193,7 @@ class AccountsManagerChangeNumberIntegrationTest {
final int rotatedPniRegistrationId = 17;
final ECKeyPair rotatedPniIdentityKeyPair = Curve.generateKeyPair();
final ECSignedPreKey rotatedSignedPreKey = KeysHelper.signedECPreKey(1L, rotatedPniIdentityKeyPair);
final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, rotatedPniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));
final AccountAttributes accountAttributes = new AccountAttributes(true, rotatedPniRegistrationId + 1, rotatedPniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false, false));
final Account account = AccountsHelper.createAccount(accountsManager, originalNumber, accountAttributes);
keysManager.storeEcSignedPreKeys(account.getIdentifier(IdentityType.ACI),

View File

@@ -156,7 +156,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
null,
"password",
null,
new Device.DeviceCapabilities(false, false, false, false),
new Device.DeviceCapabilities(false, false, false, false, false),
1,
2,
true,

View File

@@ -876,7 +876,7 @@ class AccountsManagerTest {
@ValueSource(booleans = {true, false})
void testCreateWithStorageCapability(final boolean hasStorage) throws InterruptedException {
final AccountAttributes attributes = new AccountAttributes(false, 1, 2, null, null,
true, new DeviceCapabilities(hasStorage, false, false, false));
true, new DeviceCapabilities(hasStorage, false, false, false, false));
final Account account = createAccount("+18005550123", attributes);
@@ -901,7 +901,7 @@ class AccountsManagerTest {
final byte[] deviceNameCiphertext = "device-name".getBytes(StandardCharsets.UTF_8);
final String password = "password";
final String signalAgent = "OWT";
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false);
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false, false);
final int aciRegistrationId = 17;
final int pniRegistrationId = 19;
final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciKeyPair);

View File

@@ -172,7 +172,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8),
"password",
"OWT",
new Device.DeviceCapabilities(true, true, true, false),
new Device.DeviceCapabilities(true, true, true, false, false),
1,
2,
true,
@@ -215,7 +215,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8),
"password",
"OWT",
new Device.DeviceCapabilities(true, true, true, false),
new Device.DeviceCapabilities(true, true, true, false, false),
1,
2,
true,
@@ -268,7 +268,7 @@ public class AddRemoveDeviceIntegrationTest {
"device-name".getBytes(StandardCharsets.UTF_8),
"password",
"OWT",
new Device.DeviceCapabilities(true, true, true, false),
new Device.DeviceCapabilities(true, true, true, false, false),
1,
2,
true,

View File

@@ -149,6 +149,7 @@ public class AccountsHelper {
case "getNextDeviceId" -> when(updatedAccount.getNextDeviceId()).thenAnswer(stubbing);
case "isPaymentActivationSupported" -> when(updatedAccount.isPaymentActivationSupported()).thenAnswer(stubbing);
case "isDeleteSyncSupported" -> when(updatedAccount.isDeleteSyncSupported()).thenAnswer(stubbing);
case "isVersionedExpirationTimerSupported" -> when(updatedAccount.isVersionedExpirationTimerSupported()).thenAnswer(stubbing);
case "getRegistrationLock" -> when(updatedAccount.getRegistrationLock()).thenAnswer(stubbing);
case "getIdentityKey" ->
when(updatedAccount.getIdentityKey(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing);