Skip to content

Commit

Permalink
Add delete sync capability
Browse files Browse the repository at this point in the history
  • Loading branch information
katherine-signal committed Jun 12, 2024
1 parent 1554503 commit 0414da8
Show file tree
Hide file tree
Showing 23 changed files with 144 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public UUID pniUuid() {
}

public AccountAttributes accountAttributes() {
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false))
return new AccountAttributes(true, registrationId, pniRegistrationId, "".getBytes(StandardCharsets.UTF_8), "", true, new Device.DeviceCapabilities(false, false, false, false))
.withUnidentifiedAccessKey(unidentifiedAccessKey)
.withRecoveryPassword(registrationPassword);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAu

if (capabilities == null) {
throw new WebApplicationException(Response.status(422, "Missing device capabilities").build());
} else if (isCapabilityDowngrade(account, capabilities)) {
throw new WebApplicationException(Response.status(409).build());
}

final String signalAgent;
Expand Down Expand Up @@ -387,6 +389,10 @@ Optional<UUID> checkVerificationToken(final String verificationToken) {
return Optional.of(aci);
}

private static boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities) {
return account.isDeleteSyncSupported() && !capabilities.deleteSync();
}

private static String getUsedTokenKey(final String token) {
return "usedToken::" + token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ private BaseProfileResponse buildBaseProfileResponseForAccountIdentity(final Acc
return new BaseProfileResponse(account.getIdentityKey(IdentityType.ACI),
account.getUnidentifiedAccessKey().map(UnidentifiedAccessChecksum::generateFor).orElse(null),
account.isUnrestrictedUnidentifiedAccess(),
new UserCapabilities(),
UserCapabilities.createForAccount(account),
profileBadgeConverter.convert(
getAcceptableLanguagesForRequest(containerRequestContext),
account.getBadges(),
Expand All @@ -459,7 +459,7 @@ private BaseProfileResponse buildBaseProfileResponseForPhoneNumberIdentity(final
return new BaseProfileResponse(account.getIdentityKey(IdentityType.PNI),
null,
false,
new UserCapabilities(),
UserCapabilities.createForAccount(account),
Collections.emptyList(),
new PniServiceIdentifier(account.getPhoneNumberIdentifier()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@

package org.whispersystems.textsecuregcm.entities;

import org.whispersystems.textsecuregcm.storage.Account;

public record UserCapabilities(
// TODO: Remove the paymentActivation capability entirely sometime soon after 2024-06-30
boolean paymentActivation) {
boolean paymentActivation,
boolean deleteSync) {

public UserCapabilities() {
this(true);
public static UserCapabilities createForAccount(final Account account) {
return new UserCapabilities(true, account.isDeleteSyncSupported());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ public Mono<SetCapabilitiesResponse> setCapabilities(final SetCapabilitiesReques
d -> d.setCapabilities(new Device.DeviceCapabilities(
request.getStorage(),
request.getTransfer(),
request.getPaymentActivation())))))
request.getPaymentActivation(),
request.getDeleteSync())))))
.thenReturn(SetCapabilitiesResponse.newBuilder().build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ static List<Badge> buildBadges(final List<org.whispersystems.textsecuregcm.entit
static UserCapabilities buildUserCapabilities(final org.whispersystems.textsecuregcm.entities.UserCapabilities capabilities) {
return UserCapabilities.newBuilder()
.setPaymentActivation(capabilities.paymentActivation())
.setDeleteSync(capabilities.deleteSync())
.build();
}

Expand All @@ -104,7 +105,7 @@ static GetUnversionedProfileResponse buildUnversionedProfileResponse(
final ProfileBadgeConverter profileBadgeConverter) {
final GetUnversionedProfileResponse.Builder responseBuilder = GetUnversionedProfileResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(targetIdentifier.identityType()).serialize()))
.setCapabilities(buildUserCapabilities(new org.whispersystems.textsecuregcm.entities.UserCapabilities()));
.setCapabilities(buildUserCapabilities(org.whispersystems.textsecuregcm.entities.UserCapabilities.createForAccount(targetAccount)));

switch (targetIdentifier.identityType()) {
case ACI -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ public boolean isPaymentActivationSupported() {
return allEnabledDevicesHaveCapability(DeviceCapabilities::paymentActivation);
}

public boolean isDeleteSyncSupported() {
return allEnabledDevicesHaveCapability(DeviceCapabilities::deleteSync);
}

private boolean allEnabledDevicesHaveCapability(final Predicate<DeviceCapabilities> predicate) {
requireNotStale();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,6 @@ public String getUserAgent() {
return this.userAgent;
}

public record DeviceCapabilities(boolean storage, boolean transfer, boolean paymentActivation) {
public record DeviceCapabilities(boolean storage, boolean transfer, boolean paymentActivation, boolean deleteSync) {
}
}
1 change: 1 addition & 0 deletions service/src/main/proto/org/signal/chat/device.proto
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ message SetCapabilitiesRequest {
bool storage = 1;
bool transfer = 2;
bool paymentActivation = 3;
bool deleteSync = 4;
}

message SetCapabilitiesResponse {}
4 changes: 4 additions & 0 deletions service/src/main/proto/org/signal/chat/profile.proto
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ message UserCapabilities {
* Whether all devices linked to the account support MobileCoin payments.
*/
bool payment_activation = 1;
/**
* Whether all devices linked to the account support delete syncing
*/
bool delete_sync = 2;
}

message Badge {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,8 @@ void linkDeviceAtomic(final boolean fetchesMessages,

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));
final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null,
null, true, new DeviceCapabilities(true, true, true, false));

final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
accountAttributes,
Expand Down Expand Up @@ -264,6 +265,58 @@ private static Stream<Arguments> linkDeviceAtomic() {
);
}

@ParameterizedTest
@MethodSource
void deviceDowngradeDeleteSync(final boolean accountSupportsDeleteSync, final boolean deviceSupportsDeleteSync, 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(accountSupportsDeleteSync);

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 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> deviceDowngradeDeleteSync() {
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));
Expand Down Expand Up @@ -633,7 +686,7 @@ void linkDeviceRegistrationId(final int registrationId, final int pniRegistratio
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)),
new AccountAttributes(false, registrationId, pniRegistrationId, null, null, true, new DeviceCapabilities(true, true, true, false)),
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.of(new ApnRegistrationId("apn", null)), Optional.empty()));

try (final Response response = resources.getJerseyTest()
Expand Down Expand Up @@ -692,7 +745,7 @@ void maxDevicesTest() {

@Test
void putCapabilitiesSuccessTest() {
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true);
final DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, false);
final Response response = resources
.getJerseyTest()
.target("/v1/devices/capabilities")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ class ProfileControllerTest {

private DynamicPaymentsConfiguration dynamicPaymentsConfiguration;
private Account profileAccount;
private Account capabilitiesAccount;

private static final ResourceExtension resources = ResourceExtension.builder()
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
Expand Down Expand Up @@ -203,12 +204,11 @@ void setup() {
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));

Account capabilitiesAccount = mock(Account.class);
capabilitiesAccount = mock(Account.class);

when(capabilitiesAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID);
when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY);
when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY);
when(capabilitiesAccount.isPaymentActivationSupported()).thenReturn(false);
when(capabilitiesAccount.isEnabled()).thenReturn(true);

when(accountsManager.getByServiceIdentifier(any())).thenReturn(Optional.empty());
Expand Down Expand Up @@ -439,14 +439,17 @@ void testProfileGetUnauthorized() {
assertThat(response.getStatus()).isEqualTo(401);
}

@Test
void testProfileCapabilities() {
@ParameterizedTest
@ValueSource(booleans = {true, false})
void testProfileCapabilities(final boolean isDeleteSyncSupported) {
when(capabilitiesAccount.isDeleteSyncSupported()).thenReturn(isDeleteSyncSupported);
final BaseProfileResponse profile = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_UUID)
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(BaseProfileResponse.class);

assertEquals(isDeleteSyncSupported, profile.getCapabilities().deleteSync());
assertThat(profile.getCapabilities().paymentActivation()).isTrue();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,10 +477,10 @@ static Stream<Arguments> atomicAccountCreationConflictingChannel() {
}

final AccountAttributes fetchesMessagesAccountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false));
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(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));
new AccountAttributes(false, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));

return Stream.of(
// "Fetches messages" is true, but an APNs token is provided
Expand Down Expand Up @@ -566,7 +566,7 @@ static Stream<Arguments> atomicAccountCreationPartialSignedPreKeys() {
}

final AccountAttributes accountAttributes =
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false));
new AccountAttributes(true, 1, 1, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));

return Stream.of(
// Signed PNI EC pre-key is missing
Expand Down Expand Up @@ -736,13 +736,13 @@ private static Stream<Arguments> atomicAccountCreationSuccess() {
final int registrationId = 1;
final int pniRegistrationId = 2;

final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false);
final Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(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));
new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(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));
new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, new Device.DeviceCapabilities(false, false, false, false));

final String apnsToken = "apns-token";
final String apnsVoipToken = "apns-voip-token";
Expand Down Expand Up @@ -857,7 +857,7 @@ private static String requestJson(final String sessionId,
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));
true, new Device.DeviceCapabilities(true, true, true, false));

final RegistrationRequest request = new RegistrationRequest(
Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@ void setCapabilities(
@CartesianTest.Values(bytes = {Device.PRIMARY_ID, Device.PRIMARY_ID + 1}) final byte deviceId,
@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 paymentActivation,
@CartesianTest.Values(booleans = {true, false}) final boolean deleteSync) {

mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId);

Expand All @@ -404,12 +405,14 @@ void setCapabilities(
.setStorage(storage)
.setTransfer(transfer)
.setPaymentActivation(paymentActivation)
.setDeleteSync(deleteSync)
.build());

final Device.DeviceCapabilities expectedCapabilities = new Device.DeviceCapabilities(
storage,
transfer,
paymentActivation);
paymentActivation,
deleteSync);

verify(device).setCapabilities(expectedCapabilities);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ void getUnversionedProfileUnidentifiedAccessKey() {
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum))
.setUnrestrictedUnidentifiedAccess(false)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities()))
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build();

Expand Down Expand Up @@ -214,7 +214,7 @@ void getUnversionedProfileGroupSendEndorsement() throws Exception {
final GetUnversionedProfileResponse expectedResponse = GetUnversionedProfileResponse.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnrestrictedUnidentifiedAccess(false)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities()))
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ void getUnversionedProfile(final IdentityType identityType) {
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setUnidentifiedAccess(ByteString.copyFrom(unidentifiedAccessChecksum))
.setUnrestrictedUnidentifiedAccess(true)
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(new UserCapabilities()))
.setCapabilities(ProfileGrpcHelper.buildUserCapabilities(UserCapabilities.createForAccount(account)))
.addAllBadges(ProfileGrpcHelper.buildBadges(badges))
.build();

Expand Down
Loading

0 comments on commit 0414da8

Please sign in to comment.