Convert chat-based backup calls to WebSocket.

This commit is contained in:
Cody Henthorne
2025-03-07 15:59:03 -05:00
committed by Greyson Parrelli
parent 305b380fef
commit cf78c76bbb
9 changed files with 146 additions and 257 deletions

View File

@@ -382,7 +382,7 @@ object AppDependencies {
fun provideScheduledMessageManager(): ScheduledMessageManager
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
fun provideBillingApi(): BillingApi
fun provideArchiveApi(pushServiceSocket: PushServiceSocket): ArchiveApi
fun provideArchiveApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ArchiveApi
fun provideKeysApi(pushServiceSocket: PushServiceSocket): KeysApi
fun provideAttachmentApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): AttachmentApi
fun provideLinkDeviceApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): LinkDeviceApi

View File

@@ -465,8 +465,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
}
@Override
public @NonNull ArchiveApi provideArchiveApi(@NonNull PushServiceSocket pushServiceSocket) {
return new ArchiveApi(pushServiceSocket);
public @NonNull ArchiveApi provideArchiveApi(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket, @NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket, @NonNull PushServiceSocket pushServiceSocket) {
return new ArchiveApi(authWebSocket, unauthWebSocket, pushServiceSocket);
}
@Override

View File

@@ -136,7 +136,7 @@ class NetworkDependenciesModule(
}
val archiveApi: ArchiveApi by lazy {
provider.provideArchiveApi(pushServiceSocket)
provider.provideArchiveApi(authWebSocket, unauthWebSocket, pushServiceSocket)
}
val keysApi: KeysApi by lazy {

View File

@@ -875,6 +875,8 @@ class RegistrationViewModel : ViewModel() {
SignalStore.registration.localRegistrationMetadata = metadata
RegistrationRepository.registerAccountLocally(context, metadata)
AppDependencies.authWebSocket.connect()
if (!remoteResult.storageCapable && SignalStore.registration.restoreDecisionState.isDecisionPending) {
Log.v(TAG, "Not storage capable and still pending restore decision, likely an account with no data to restore, skipping post register restore")
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount

View File

@@ -216,7 +216,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
return mockk(relaxed = true)
}
override fun provideArchiveApi(pushServiceSocket: PushServiceSocket): ArchiveApi {
override fun provideArchiveApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ArchiveApi {
return mockk(relaxed = true)
}

View File

@@ -120,6 +120,19 @@ sealed class NetworkResult<T>(
ApplicationError(e)
}
/**
* Wraps a local operation, [block], that may throw an exception that should be wrapped in an [ApplicationError]
* and abort downstream network requests that directly depend on the output of the local operation. Should
* be used almost exclusively prior to a [then].
*/
fun <T : Any> fromLocal(block: () -> T): NetworkResult<T> {
return try {
Success(block())
} catch (e: Throwable) {
ApplicationError(e)
}
}
/**
* Runs [operation] to perform a network call. If [shouldRetry] returns false for the result, then returns it. Otherwise will call [operation] repeatedly
* until [shouldRetry] returns false or is called [maxAttempts] number of times.

View File

@@ -5,6 +5,7 @@
package org.whispersystems.signalservice.api.archive
import org.signal.core.util.isNotNullOrBlank
import org.signal.libsignal.protocol.ecc.ECPrivateKey
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.zkgroup.GenericServerPublicParams
@@ -18,26 +19,31 @@ import org.whispersystems.signalservice.api.backup.MediaRootBackupKey
import org.whispersystems.signalservice.api.backup.MessageBackupKey
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.delete
import org.whispersystems.signalservice.internal.get
import org.whispersystems.signalservice.internal.post
import org.whispersystems.signalservice.internal.push.AttachmentUploadForm
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.put
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.io.InputStream
import java.time.Instant
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
* Class to interact with various archive-related endpoints.
* Why is it called archive instead of backup? Because SVR took the "backup" endpoint namespace first :)
*/
class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
class ArchiveApi(
private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket,
private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket,
private val pushServiceSocket: PushServiceSocket
) {
private val backupServerPublicParams: GenericServerPublicParams = GenericServerPublicParams(pushServiceSocket.configuration.backupServerPublicParams)
companion object {
@JvmStatic
fun create(pushServiceSocket: PushServiceSocket): ArchiveApi {
return ArchiveApi(pushServiceSocket)
}
}
/**
* Retrieves a set of credentials one can use to authorize other requests.
*
@@ -56,9 +62,11 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun getServiceCredentials(currentTime: Long): NetworkResult<ArchiveServiceCredentialsResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.getArchiveCredentials(currentTime)
}
val roundedToNearestDay = currentTime.milliseconds.inWholeDays.days
val endTime = roundedToNearestDay + 7.days
val request = WebSocketRequestMessage.get("/v1/archives/auth?redemptionStartSeconds=${roundedToNearestDay.inWholeSeconds}&redemptionEndSeconds=${endTime.inWholeSeconds}")
return NetworkResult.fromWebSocketRequest(authWebSocket, request, ArchiveServiceCredentialsResponse::class)
}
/**
@@ -73,12 +81,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun getCdnReadCredentials(cdnNumber: Int, aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<GetArchiveCdnCredentialsResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.get("/v1/archives/auth/read?cdn=$cdnNumber", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, GetArchiveCdnCredentialsResponse::class)
}
}
/**
@@ -92,11 +100,15 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun triggerBackupIdReservation(messageBackupKey: MessageBackupKey, mediaRootBackupKey: MediaRootBackupKey, aci: ACI): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val messageBackupRequestContext = BackupAuthCredentialRequestContext.create(messageBackupKey.value, aci.rawUuid)
val mediaBackupRequestContext = BackupAuthCredentialRequestContext.create(mediaRootBackupKey.value, aci.rawUuid)
pushServiceSocket.setArchiveBackupId(messageBackupRequestContext.request, mediaBackupRequestContext.request)
}
val messageBackupRequestContext = BackupAuthCredentialRequestContext.create(messageBackupKey.value, aci.rawUuid)
val mediaBackupRequestContext = BackupAuthCredentialRequestContext.create(mediaRootBackupKey.value, aci.rawUuid)
val request = WebSocketRequestMessage.put(
"/v1/archives/backupid",
ArchiveSetBackupIdRequest(messageBackupRequestContext.request, mediaBackupRequestContext.request)
)
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
}
/**
@@ -113,11 +125,14 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun setPublicKey(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.setArchivePublicKey(presentationData.publicKey, presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.then { presentation ->
val headers = presentation.toArchiveCredentialPresentation().toHeaders()
val publicKey = presentation.publicKey
val request = WebSocketRequestMessage.put("/v1/archives/keys", ArchiveSetPublicKeyRequest(publicKey), headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
}
/**
@@ -130,11 +145,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun getMessageBackupUploadForm(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<AttachmentUploadForm> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.get("/v1/archives/upload/form", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, AttachmentUploadForm::class)
}
}
/**
@@ -144,11 +160,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a backup yet.
*/
fun getBackupInfo(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<ArchiveGetBackupInfoResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.get("/v1/archives", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, ArchiveGetBackupInfoResponse::class)
}
}
/**
@@ -165,11 +182,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate limited.
*/
fun refreshBackup(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.refreshBackup(presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.post(path = "/v1/archives", body = null, headers = headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
}
/**
@@ -186,22 +204,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
*
*/
fun deleteBackup(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MessageBackupKey>): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.deleteBackup(presentationData.toArchiveCredentialPresentation())
}
}
/**
* Lists the media objects in the backup
*/
fun listMediaObjects(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>, limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor)
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.delete("/v1/archives", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
}
/**
@@ -238,11 +246,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* - 429: Rate-limited
*/
fun getMediaUploadForm(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>): NetworkResult<AttachmentUploadForm> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation())
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.get("/v1/archives/media/upload/form", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, AttachmentUploadForm::class)
}
}
/**
@@ -270,12 +279,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
* @param cursor A token that can be read from your previous response, telling the server where to start the next page.
*/
fun getArchiveMediaItemsPage(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>, limit: Int, cursor: String?): NetworkResult<ArchiveGetMediaItemsResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor)
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.get("/v1/archives/media?limit=$limit${if (cursor.isNotNullOrBlank()) "&cursor=$cursor" else ""}", headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, ArchiveGetMediaItemsResponse::class)
}
}
/**
@@ -294,12 +303,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>,
item: ArchiveMediaRequest
): NetworkResult<ArchiveMediaResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item)
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.put("/v1/archives/media", item, headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, ArchiveMediaResponse::class)
}
}
/**
@@ -310,14 +319,12 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>,
items: List<ArchiveMediaRequest>
): NetworkResult<BatchArchiveMediaResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
val request = BatchArchiveMediaRequest(items = items)
pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), request)
}
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.put("/v1/archives/media/batch", BatchArchiveMediaRequest(items = items), headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request, BatchArchiveMediaResponse::class)
}
}
/**
@@ -335,12 +342,18 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) {
archiveServiceAccess: ArchiveServiceAccess<MediaRootBackupKey>,
mediaToDelete: List<DeleteArchivedMediaRequest.ArchivedMediaObject>
): NetworkResult<Unit> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
val presentationData = CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete)
return getCredentialPresentation(aci, archiveServiceAccess)
.map { it.toArchiveCredentialPresentation().toHeaders() }
.then { headers ->
val request = WebSocketRequestMessage.post("/v1/archives/media/delete", DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete), headers)
NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
}
pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request)
private fun getCredentialPresentation(aci: ACI, archiveServiceAccess: ArchiveServiceAccess<*>): NetworkResult<CredentialPresentationData> {
return NetworkResult.fromLocal {
val zkCredential = getZkCredential(aci, archiveServiceAccess)
CredentialPresentationData.from(archiveServiceAccess.backupKey, aci, zkCredential, backupServerPublicParams)
}
}

View File

@@ -12,10 +12,24 @@ import java.security.SecureRandom
/**
* Create a basic GET web socket request
*/
fun WebSocketRequestMessage.Companion.get(path: String): WebSocketRequestMessage {
fun WebSocketRequestMessage.Companion.get(path: String, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "GET",
path = path,
headers = headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
/**
* Create a basic POST web socket request
*/
fun WebSocketRequestMessage.Companion.post(path: String, body: Any?, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "POST",
path = path,
body = body?.let { JsonUtil.toJsonByteString(body) },
headers = (if (body != null) listOf("content-type:application/json") else emptyList()) + headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
@@ -23,10 +37,11 @@ fun WebSocketRequestMessage.Companion.get(path: String): WebSocketRequestMessage
/**
* Create a basic DELETE web socket request
*/
fun WebSocketRequestMessage.Companion.delete(path: String): WebSocketRequestMessage {
fun WebSocketRequestMessage.Companion.delete(path: String, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "DELETE",
path = path,
headers = headers.toHeaderList(),
id = SecureRandom().nextLong()
)
}
@@ -34,12 +49,16 @@ fun WebSocketRequestMessage.Companion.delete(path: String): WebSocketRequestMess
/**
* Create a basic PUT web socket request, where body is JSON-ified.
*/
fun WebSocketRequestMessage.Companion.put(path: String, body: Any): WebSocketRequestMessage {
fun WebSocketRequestMessage.Companion.put(path: String, body: Any, headers: Map<String, String> = emptyMap()): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "PUT",
path = path,
headers = listOf("content-type:application/json"),
headers = listOf("content-type:application/json") + headers.toHeaderList(),
body = JsonUtil.toJsonByteString(body),
id = SecureRandom().nextLong()
)
}
private fun Map<String, String>.toHeaderList(): List<String> {
return map { (key, value) -> "$key:$value" }
}

View File

@@ -22,7 +22,6 @@ import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.protocol.state.PreKeyBundle;
import org.signal.libsignal.protocol.util.Pair;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest;
import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
@@ -47,17 +46,12 @@ import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.archive.ArchiveCredentialPresentation;
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse;
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse;
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest;
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse;
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredentialsResponse;
import org.whispersystems.signalservice.api.archive.ArchiveSetBackupIdRequest;
import org.whispersystems.signalservice.api.archive.ArchiveSetPublicKeyRequest;
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaRequest;
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse;
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest;
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
@@ -230,6 +224,7 @@ public class PushServiceSocket {
private static final String ATTACHMENT_V4_PATH = "/v4/attachments/form/upload";
private static final String PAYMENTS_AUTH_PATH = "/v1/payments/auth";
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
private static final String PROFILE_PATH = "/v1/profile/%s";
private static final String PROFILE_BATCH_CHECK_PATH = "/v1/profile/identity_check/batch";
@@ -239,7 +234,6 @@ public class PushServiceSocket {
private static final String ATTACHMENT_KEY_DOWNLOAD_PATH = "attachments/%s";
private static final String ATTACHMENT_ID_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
private static final String AVATAR_UPLOAD_PATH = "";
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
@@ -254,9 +248,6 @@ public class PushServiceSocket {
private static final String GROUPSV2_TOKEN = "/v2/groups/token";
private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version";
private static final String PAYMENTS_CONVERSIONS = "/v1/payments/conversions";
private static final String SUBMIT_RATE_LIMIT_CHALLENGE = "/v1/challenge";
private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push";
@@ -293,22 +284,10 @@ public class PushServiceSocket {
private static final String BACKUP_AUTH_CHECK_V2 = "/v2/backup/auth/check";
private static final String BACKUP_AUTH_CHECK_V3 = "/v3/backup/auth/check";
private static final String ARCHIVE_CREDENTIALS = "/v1/archives/auth?redemptionStartSeconds=%d&redemptionEndSeconds=%d";
private static final String ARCHIVE_READ_CREDENTIALS = "/v1/archives/auth/read?cdn=%d";
private static final String ARCHIVE_BACKUP_ID = "/v1/archives/backupid";
private static final String ARCHIVE_PUBLIC_KEY = "/v1/archives/keys";
private static final String ARCHIVE_INFO = "/v1/archives";
private static final String ARCHIVE_MESSAGE_UPLOAD_FORM = "/v1/archives/upload/form";
private static final String ARCHIVE_MEDIA_UPLOAD_FORM = "/v1/archives/media/upload/form";
private static final String ARCHIVE_MEDIA = "/v1/archives/media";
private static final String ARCHIVE_MEDIA_LIST = "/v1/archives/media?limit=%d";
private static final String ARCHIVE_MEDIA_BATCH = "/v1/archives/media/batch";
private static final String ARCHIVE_MEDIA_DELETE = "/v1/archives/media/delete";
private static final String ARCHIVE_MEDIA_DOWNLOAD_PATH = "backups/%s/%s";
private static final String SET_SHARE_SET_PATH = "/v3/backup/share-set";
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
@@ -483,139 +462,6 @@ public class PushServiceSocket {
return JsonUtil.fromJsonResponse(body, Svr3Credentials.class);
}
public ArchiveServiceCredentialsResponse getArchiveCredentials(long currentTime) throws IOException {
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7);
String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class);
}
public void setArchiveBackupId(BackupAuthCredentialRequest messageRequest, BackupAuthCredentialRequest mediaRequest) throws IOException {
String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(messageRequest, mediaRequest));
makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
}
public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String body = JsonUtil.toJson(new ArchiveSetPublicKeyRequest(publicKey));
makeServiceRequestWithoutAuthentication(ARCHIVE_PUBLIC_KEY, "PUT", body, headers, NO_HANDLER);
}
public ArchiveGetBackupInfoResponse getArchiveBackupInfo(ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(response, ArchiveGetBackupInfoResponse.class);
}
/**
* POST credential presentation to the server to keep backup alive.
*/
public void refreshBackup(ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "POST", null, headers, UNOPINIONATED_HANDLER);
}
/**
* DELETE credential presentation to the server to delete backup.
*/
public void deleteBackup(ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "DELETE", null, headers, UNOPINIONATED_HANDLER);
}
public List<ArchiveGetMediaItemsResponse.StoredMediaObject> debugGetAllArchiveMediaItems(ArchiveCredentialPresentation credentialPresentation) throws IOException {
List<ArchiveGetMediaItemsResponse.StoredMediaObject> mediaObjects = new ArrayList<>();
String cursor = null;
do {
ArchiveGetMediaItemsResponse response = getArchiveMediaItemsPage(credentialPresentation, 512, cursor);
mediaObjects.addAll(response.getStoredMediaObjects());
cursor = response.getCursor();
} while (cursor != null);
return mediaObjects;
}
/**
* Retrieves a page of media items in the user's archive.
* @param cursor A token that can be read from your previous response, telling the server where to start the next page.
*/
public ArchiveGetMediaItemsResponse getArchiveMediaItemsPage(ArchiveCredentialPresentation credentialPresentation, int limit, String cursor) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String url = String.format(Locale.US, ARCHIVE_MEDIA_LIST, limit);
if (cursor != null) {
url += "&cursor=" + cursor;
}
String response = makeServiceRequestWithoutAuthentication(url, "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(response, ArchiveGetMediaItemsResponse.class);
}
/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
*/
public ArchiveMediaResponse archiveAttachmentMedia(@Nonnull ArchiveCredentialPresentation credentialPresentation, @Nonnull ArchiveMediaRequest request) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA, "PUT", JsonUtil.toJson(request), headers, UNOPINIONATED_HANDLER);
return JsonUtil.fromJson(response, ArchiveMediaResponse.class);
}
/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
*/
public BatchArchiveMediaResponse archiveAttachmentMedia(@Nonnull ArchiveCredentialPresentation credentialPresentation, @Nonnull BatchArchiveMediaRequest request) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA_BATCH, "PUT", JsonUtil.toJson(request), headers, UNOPINIONATED_HANDLER);
return JsonUtil.fromJson(response, BatchArchiveMediaResponse.class);
}
/**
* Delete media from the backup cdn.
*/
public void deleteArchivedMedia(@Nonnull ArchiveCredentialPresentation credentialPresentation, @Nonnull DeleteArchivedMediaRequest request) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA_DELETE, "POST", JsonUtil.toJson(request), headers, NO_HANDLER);
}
public AttachmentUploadForm getArchiveMessageBackupUploadForm(ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MESSAGE_UPLOAD_FORM, "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(response, AttachmentUploadForm.class);
}
public AttachmentUploadForm getArchiveMediaUploadForm(@NotNull ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MEDIA_UPLOAD_FORM, "GET", null, headers, UNOPINIONATED_HANDLER);
return JsonUtil.fromJson(response, AttachmentUploadForm.class);
}
/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
*/
public GetArchiveCdnCredentialsResponse getArchiveCdnReadCredentials(int cdnNumber, @Nonnull ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(String.format(Locale.US, ARCHIVE_READ_CREDENTIALS, cdnNumber), "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(response, GetArchiveCdnCredentialsResponse.class);
}
public void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException {
String body = JsonUtil.toJson(request);
makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
@@ -1308,10 +1154,6 @@ public class PushServiceSocket {
return token;
}
private String getCredentials(String authPath) throws IOException {
return getAuthCredentials(authPath).asBasic();
}
public AuthCredentials getPaymentsAuthorization() throws IOException {
return getAuthCredentials(PAYMENTS_AUTH_PATH);
}