Allow primary devices to change names of linked devices

This commit is contained in:
Jon Chambers
2024-10-29 09:52:38 -04:00
committed by GitHub
parent 712f3affd9
commit f3b22e04e8
14 changed files with 231 additions and 41 deletions

View File

@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.Base64;
import java.util.Objects;
@@ -19,6 +20,7 @@ import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
@@ -27,6 +29,7 @@ import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -192,10 +195,39 @@ public class AccountController {
@PUT
@Path("/name/")
public void setName(@Mutable @Auth AuthenticatedDevice auth, @NotNull @Valid DeviceName deviceName) {
Account account = auth.getAccount();
Device device = auth.getAuthenticatedDevice();
accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
@Operation(summary = "Set a device's encrypted name",
description = """
Sets the encrypted name for the specified device. Primary devices may change the name of any device associated
with their account, but linked devices may only change their own name. If no device ID is specified, then the
authenticated device's ID will be used.
""")
@ApiResponse(responseCode = "204", description = "Device name changed successfully")
@ApiResponse(responseCode = "404", description = "No device found with the given ID")
@ApiResponse(responseCode = "403", description = "Not authorized to change the name of the device with the given ID")
public void setName(@Mutable @Auth final AuthenticatedDevice auth,
@NotNull @Valid final DeviceName deviceName,
@Nullable
@QueryParam("deviceId")
@Schema(description = "The ID of the device for which to set a name; if omitted, the authenticated device will be targeted for a name change",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
final Byte deviceId) {
final Account account = auth.getAccount();
final byte targetDeviceId = deviceId == null ? auth.getAuthenticatedDevice().getId() : deviceId;
if (account.getDevice(targetDeviceId).isEmpty()) {
throw new NotFoundException();
}
final boolean mayChangeName = auth.getAuthenticatedDevice().isPrimary() ||
auth.getAuthenticatedDevice().getId() == targetDeviceId;
if (!mayChangeName) {
throw new ForbiddenException();
}
accounts.updateDevice(account, targetDeviceId, d -> d.setName(deviceName.deviceName()));
}
@PUT

View File

@@ -5,25 +5,15 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
public class DeviceName {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotEmpty
@Size(max = 225)
private byte[] deviceName;
public DeviceName() {}
public byte[] getDeviceName() {
return deviceName;
}
public record DeviceName(@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotEmpty
@Size(max = 225)
byte[] deviceName) {
}

View File

@@ -6,13 +6,15 @@
package org.whispersystems.textsecuregcm.grpc;
import io.grpc.Status;
import org.whispersystems.textsecuregcm.storage.Device;
public class DeviceIdUtil {
static byte validate(int deviceId) {
if (deviceId > Byte.MAX_VALUE) {
if (deviceId < Device.PRIMARY_ID || deviceId > Byte.MAX_VALUE) {
throw Status.INVALID_ARGUMENT.withDescription("Device ID is out of range").asRuntimeException();
}
return (byte) deviceId;
}
}

View File

@@ -89,6 +89,17 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
public Mono<SetDeviceNameResponse> setDeviceName(final SetDeviceNameRequest request) {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
final byte deviceId = DeviceIdUtil.validate(request.getId());
final boolean mayChangeName = authenticatedDevice.deviceId() == Device.PRIMARY_ID ||
authenticatedDevice.deviceId() == deviceId;
if (!mayChangeName) {
throw Status.PERMISSION_DENIED
.withDescription("Authenticated device is not authorized to change target device name")
.asRuntimeException();
}
if (request.getName().isEmpty()) {
throw Status.INVALID_ARGUMENT.withDescription("Must specify a device name").asRuntimeException();
}
@@ -100,7 +111,12 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(),
.doOnNext(account -> {
if (account.getDevice(deviceId).isEmpty()) {
throw Status.NOT_FOUND.withDescription("No device found with given ID").asRuntimeException();
}
})
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, deviceId,
device -> device.setName(request.getName().toByteArray()))))
.thenReturn(SetDeviceNameResponse.newBuilder().build());
}

View File

@@ -47,7 +47,9 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
final ServiceIdentifier serviceIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getTargetIdentifier());
final byte deviceId = DeviceIdUtil.validate(request.getRequest().getDeviceId());
final byte deviceId = request.getRequest().hasDeviceId()
? DeviceIdUtil.validate(request.getRequest().getDeviceId())
: KeysGrpcHelper.ALL_DEVICES;
return switch (request.getAuthorizationCase()) {
case GROUP_SEND_TOKEN ->

View File

@@ -5,7 +5,6 @@
package org.whispersystems.textsecuregcm.grpc;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import org.signal.chat.common.EcPreKey;
@@ -26,7 +25,6 @@ import reactor.util.function.Tuples;
class KeysGrpcHelper {
@VisibleForTesting
static final byte ALL_DEVICES = 0;
static Mono<GetPreKeysResponse> getPreKeys(final Account targetAccount,

View File

@@ -122,7 +122,9 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase {
final ServiceIdentifier targetIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier());
final byte deviceId = DeviceIdUtil.validate(request.getDeviceId());
final byte deviceId = request.hasDeviceId()
? DeviceIdUtil.validate(request.getDeviceId())
: KeysGrpcHelper.ALL_DEVICES;
final String rateLimitKey = authenticatedDevice.accountIdentifier() + "." +
authenticatedDevice.deviceId() + "__" +