Document `ProvisioningController` and `ProvisioningConnectListener`
This commit is contained in:
parent
7a6ce00fed
commit
0a1161048f
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue