Add rudimentary link+sync support.

This commit is contained in:
Cody Henthorne
2025-08-06 12:51:09 -04:00
parent 1a68b8768d
commit 7ca1ac4efb
10 changed files with 240 additions and 5 deletions

View File

@@ -7,7 +7,6 @@
package org.whispersystems.signalservice.api;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
@@ -17,6 +16,7 @@ import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifest;
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
@@ -38,6 +38,8 @@ import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import kotlin.Unit;
/**
* The primary interface for receiving Signal Service messages.
*
@@ -200,6 +202,16 @@ public class SignalServiceMessageReceiver {
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
}
/**
* Retrieves a link+sync backup file. The data is written to @{code destination}.
*/
public @Nonnull NetworkResult<Unit> retrieveLinkAndSyncBackup(int cdn, @Nonnull String key, @Nonnull File destination, @Nullable ProgressListener listener) {
return NetworkResult.fromFetch(() -> {
socket.retrieveAttachment(cdn, Collections.emptyMap(), new SignalServiceAttachmentRemoteId.V4(key), destination, 1_000_000_000L, listener);
return Unit.INSTANCE;
});
}
public @Nonnull ZonedDateTime getCdnLastModifiedTime(int cdnNumber, Map<String, String> headers, String cdnPath) throws MissingConfigurationException, IOException {
return socket.getCdnLastModifiedTime(cdnNumber, headers, cdnPath);
}

View File

@@ -212,4 +212,27 @@ class LinkDeviceApi(
val request = WebSocketRequestMessage.put("/v1/accounts/name?deviceId=$deviceId", SetDeviceNameRequest(encryptedDeviceName))
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
}
/**
* A "long-polling" endpoint that will return once the primary device has successfully sent sync data.
*
* @param timeout The max amount of time to wait. Capped at 30 seconds.
*
* GET /v1/devices/transfer_archive?timeout=[timeout]
*
* - 200: Success, the primary device was sent backup sync data.
* - 204: The primary didn't provide data before the max waiting time elapsed.
* - 400: Invalid timeout.
* - 429: Rate-limited.
*/
fun waitForPrimaryDevice(timeout: Duration = 30.seconds): NetworkResult<TransferArchiveResponse> {
val request = WebSocketRequestMessage.get("/v1/devices/transfer_archive?timeout=${timeout.inWholeSeconds}")
return NetworkResult
.fromWebSocketRequest(
signalWebSocket = authWebSocket,
request = request,
timeout = timeout,
webSocketResponseConverter = NetworkResult.LongPollingWebSocketConverter(TransferArchiveResponse::class)
)
}
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.link
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
/**
* Data from primary on where to find link+sync backup file.
*/
data class TransferArchiveResponse @JsonCreator constructor(
@JsonProperty val cdn: Int,
@JsonProperty val key: String
)

View File

@@ -249,7 +249,7 @@ class ProvisioningSocket<T> private constructor(
private fun generateProvisioningUrl(deviceAddress: String): String {
val encodedDeviceId = URLEncoder.encode(deviceAddress, "UTF-8")
val encodedPubKey: String = URLEncoder.encode(Base64.encodeWithoutPadding(cipher.secondaryDevicePublicKey.serialize()), "UTF-8")
return "sgnl://${mode.host}?uuid=$encodedDeviceId&pub_key=$encodedPubKey"
return "sgnl://${mode.host}?uuid=$encodedDeviceId&pub_key=$encodedPubKey${mode.params}"
}
private suspend fun keepAlive(webSocket: WebSocket) {
@@ -288,9 +288,9 @@ class ProvisioningSocket<T> private constructor(
}
}
enum class Mode(val host: String) {
REREG("rereg"),
LINK("linkdevice")
enum class Mode(val host: String, val params: String) {
REREG("rereg", ""),
LINK("linkdevice", "&capabilities=backup4")
}
fun interface ProvisioningSocketExceptionHandler {