Document `ProvisioningController` and `ProvisioningConnectListener`

This commit is contained in:
Jon Chambers 2024-09-30 10:42:53 -04:00 committed by Jon Chambers
parent 7a6ce00fed
commit 0a1161048f
4 changed files with 80 additions and 8 deletions

View File

@ -57,6 +57,7 @@ import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
import org.whispersystems.textsecuregcm.entities.LinkDeviceRequest;
import org.whispersystems.textsecuregcm.entities.PreKeySignatureValidator;
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
import org.whispersystems.textsecuregcm.entities.SetPublicKeyRequest;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@ -146,9 +147,34 @@ public class DeviceController {
accounts.removeDevice(auth.getAccount(), deviceId).join();
}
/**
* Generates a signed device-linking token. Generally, primary devices will include the signed device-linking token in
* a provisioning message to a new device, and then the new device will include the token in its request to
* {@link #linkDevice(BasicAuthorizationHeader, String, LinkDeviceRequest, ContainerRequest)}.
*
* @param auth the authenticated account/device
*
* @return a signed device-linking token
*
* @throws RateLimitExceededException if the caller has made too many calls to this method in a set amount of time
* @throws DeviceLimitExceededException if the authenticated account has already reached the maximum number of linked
* devices
*
* @see ProvisioningController#sendProvisioningMessage(AuthenticatedDevice, String, ProvisioningMessage, String)
*/
@GET
@Path("/provisioning/code")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Generate a signed device-linking token",
description = """
Generate a signed device-linking token for transmission to a pending linked device via a provisioning message.
""")
@ApiResponse(responseCode="200", description="Token was generated successfully", useReturnTypeSchema=true)
@ApiResponse(responseCode = "411", description = "The authenticated account already has the maximum allowed number of linked devices")
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
name = "Retry-After",
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
public VerificationCode createDeviceToken(@ReadOnly @Auth AuthenticatedDevice auth)
throws RateLimitExceededException, DeviceLimitExceededException {

View File

@ -13,6 +13,9 @@ import io.dropwizard.auth.Auth;
import io.dropwizard.util.DataSize;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.Base64;
import javax.validation.Valid;
@ -34,6 +37,16 @@ import org.whispersystems.textsecuregcm.push.ProvisioningManager;
import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress;
import org.whispersystems.websocket.auth.ReadOnly;
/**
* The provisioning controller facilitates transmission of provisioning messages from the primary device associated with
* an existing Signal account to a new device. To send a provisioning message, a primary device generally scans a QR
* code displayed by the new device that contains the device's "provisioning address" and a public key. The primary
* device then encrypts a use-case-specific provisioning message and posts it to
* {@link #sendProvisioningMessage(AuthenticatedDevice, String, ProvisioningMessage, String)}, at which point the server
* delivers the message to the new device via an open provisioning WebSocket.
*
* @see org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener
*/
@Path("/v1/provisioning")
@Tag(name = "Provisioning")
public class ProvisioningController {
@ -56,21 +69,34 @@ public class ProvisioningController {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void sendProvisioningMessage(@ReadOnly @Auth AuthenticatedDevice auth,
@PathParam("destination") String destinationName,
@NotNull @Valid ProvisioningMessage message,
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent)
@Operation(
summary = "Send a provisioning message to a new device",
description = """
Send a provisioning message from an authenticated device to a device that (presumably) is not yet associated
with a Signal account.
""")
@ApiResponse(responseCode="204", description="The provisioning message was delivered to the given provisioning address")
@ApiResponse(responseCode="400", description="The provisioning message was too large")
@ApiResponse(responseCode="404", description="No device with the given provisioning address was connected at the time of the request")
public void sendProvisioningMessage(@ReadOnly @Auth final AuthenticatedDevice auth,
@Parameter(description = "The temporary provisioning address to which to send a provisioning message")
@PathParam("destination") final String provisioningAddress,
@Parameter(description = "The provisioning message to send to the given provisioning address")
@NotNull @Valid final ProvisioningMessage message,
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent)
throws RateLimitExceededException {
if (message.body().length() > MAX_MESSAGE_SIZE) {
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent)))
.increment();
Metrics.counter(REJECT_OVERSIZE_MESSAGE_COUNTER, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))).increment();
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
rateLimiters.getMessagesLimiter().validate(auth.getAccount().getUuid());
if (!provisioningManager.sendProvisioningMessage(ProvisioningAddress.create(destinationName),
if (!provisioningManager.sendProvisioningMessage(ProvisioningAddress.create(provisioningAddress),
Base64.getMimeDecoder().decode(message.body()))) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}

View File

@ -5,7 +5,11 @@
package org.whispersystems.textsecuregcm.entities;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
public record ProvisioningMessage(@NotEmpty String body) {
public record ProvisioningMessage(
@Schema(description = "The MIME base64-encoded body of the provisioning message to send to the destination device")
@NotEmpty
String body) {
}

View File

@ -5,7 +5,9 @@
package org.whispersystems.textsecuregcm.websocket;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.ProvisioningMessage;
import org.whispersystems.textsecuregcm.push.ProvisioningManager;
import org.whispersystems.textsecuregcm.storage.PubSubProtos;
import org.whispersystems.textsecuregcm.util.HeaderUtils;
@ -14,6 +16,20 @@ import org.whispersystems.websocket.setup.WebSocketConnectListener;
import java.util.List;
import java.util.Optional;
/**
* A "provisioning WebSocket" provides a mechanism for sending a caller-defined provisioning message from the primary
* device associated with a Signal account to a new device that is not yet associated with a Signal account. Generally,
* the message contains key material and credentials the new device needs to associate itself with the primary device's
* Signal account.
* <p>
* New devices initiate the provisioning process by opening a provisioning WebSocket. The server assigns the new device
* a random, temporary "provisioning address," which it transmits via the newly-opened WebSocket. From there, the new
* device generally displays the provisioning address (and a public key) as a QR code. After that, the primary device
* will scan the QR code and send an encrypted provisioning message to the new device via
* {@link org.whispersystems.textsecuregcm.controllers.ProvisioningController#sendProvisioningMessage(AuthenticatedDevice, String, ProvisioningMessage, String)}.
* Once the server receives the message from the primary device, it sends the message to the new device via the open
* WebSocket, then closes the WebSocket connection.
*/
public class ProvisioningConnectListener implements WebSocketConnectListener {
private final ProvisioningManager provisioningManager;