mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-01 22:25:46 +01:00
Add archive media apis.
This commit is contained in:
committed by
Alex Hart
parent
ccc9752485
commit
218964cbda
@@ -125,7 +125,7 @@ class ArchiveApi(
|
||||
* Retrieves all media items in the user's archive. Note that this could be a very large number of items, making this only suitable for debugging.
|
||||
* Use [getArchiveMediaItemsPage] in production.
|
||||
*/
|
||||
fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
|
||||
fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<List<StoredMediaObject>> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
@@ -154,7 +154,58 @@ class ArchiveApi(
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), 512, cursor)
|
||||
pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
|
||||
*/
|
||||
fun archiveAttachmentMedia(
|
||||
backupKey: BackupKey,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
item: ArchiveMediaRequest
|
||||
): NetworkResult<ArchiveMediaResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
|
||||
pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
|
||||
*/
|
||||
fun archiveAttachmentMedia(
|
||||
backupKey: BackupKey,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
items: List<ArchiveMediaRequest>
|
||||
): NetworkResult<BatchArchiveMediaResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
|
||||
val request = BatchArchiveMediaRequest(items = items)
|
||||
|
||||
pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete media from the backup cdn.
|
||||
*/
|
||||
fun deleteArchivedMedia(
|
||||
backupKey: BackupKey,
|
||||
serviceCredential: ArchiveServiceCredential,
|
||||
mediaToDelete: List<DeleteArchivedMediaRequest.ArchivedMediaObject>
|
||||
): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
val zkCredential = getZkCredential(backupKey, serviceCredential)
|
||||
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
|
||||
val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete)
|
||||
|
||||
pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
|
||||
/**
|
||||
* Acts as credentials for various archive operations.
|
||||
*/
|
||||
class ArchiveCredentialPresentation(
|
||||
val presentation: ByteArray,
|
||||
val signedPresentation: ByteArray
|
||||
)
|
||||
) {
|
||||
fun toHeaders(): MutableMap<String, String> {
|
||||
return mutableMapOf(
|
||||
"X-Signal-ZK-Auth" to Base64.encodeWithPadding(presentation),
|
||||
"X-Signal-ZK-Auth-Signature" to Base64.encodeWithPadding(signedPresentation)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Request to copy and re-encrypt media from the attachments cdn into the backup cdn.
|
||||
*/
|
||||
class ArchiveMediaRequest(
|
||||
@JsonProperty val sourceAttachment: SourceAttachment,
|
||||
@JsonProperty val objectLength: Int,
|
||||
@JsonProperty val mediaId: String,
|
||||
@JsonProperty val hmacKey: String,
|
||||
@JsonProperty val encryptionKey: String,
|
||||
@JsonProperty val iv: String
|
||||
) {
|
||||
class SourceAttachment(
|
||||
@JsonProperty val cdn: Int,
|
||||
@JsonProperty val key: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Response to archiving media, backup CDN number where media is located.
|
||||
*/
|
||||
class ArchiveMediaResponse(
|
||||
@JsonProperty val cdn: Int
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Request to copy and re-encrypt media from the attachments cdn into the backup cdn.
|
||||
*/
|
||||
class BatchArchiveMediaRequest(
|
||||
@JsonProperty val items: List<ArchiveMediaRequest>
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Multi-response data for a batch archive media operation.
|
||||
*/
|
||||
class BatchArchiveMediaResponse(
|
||||
@JsonProperty val responses: List<BatchArchiveMediaItemResponse>
|
||||
) {
|
||||
class BatchArchiveMediaItemResponse(
|
||||
@JsonProperty val status: Int?,
|
||||
@JsonProperty val failureReason: String?,
|
||||
@JsonProperty val cdn: Int?,
|
||||
@JsonProperty val mediaId: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.archive
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Delete media from the backup cdn.
|
||||
*/
|
||||
class DeleteArchivedMediaRequest(
|
||||
@JsonProperty val mediaToDelete: List<ArchivedMediaObject>
|
||||
) {
|
||||
class ArchivedMediaObject(
|
||||
@JsonProperty val cdn: Int,
|
||||
@JsonProperty val mediaId: String
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class BackupKey(val value: ByteArray) {
|
||||
require(value.size == 32) { "Backup key must be 32 bytes!" }
|
||||
}
|
||||
|
||||
fun deriveSecrets(aci: ACI): KeyMaterial {
|
||||
fun deriveSecrets(aci: ACI): KeyMaterial<BackupId> {
|
||||
val backupId = BackupId(
|
||||
HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupId".toByteArray(), 16)
|
||||
)
|
||||
@@ -24,15 +24,32 @@ class BackupKey(val value: ByteArray) {
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
|
||||
|
||||
return KeyMaterial(
|
||||
backupId = backupId,
|
||||
id = backupId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||
iv = extendedKey.copyOfRange(64, 80)
|
||||
)
|
||||
}
|
||||
|
||||
class KeyMaterial(
|
||||
val backupId: BackupId,
|
||||
fun deriveMediaId(dataHash: ByteArray): MediaId {
|
||||
return MediaId(HKDF.deriveSecrets(value, dataHash, "Media ID".toByteArray(), 15))
|
||||
}
|
||||
|
||||
fun deriveMediaSecrets(dataHash: ByteArray): KeyMaterial<MediaId> {
|
||||
val mediaId = deriveMediaId(dataHash)
|
||||
|
||||
val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80)
|
||||
|
||||
return KeyMaterial(
|
||||
id = mediaId,
|
||||
macKey = extendedKey.copyOfRange(0, 32),
|
||||
cipherKey = extendedKey.copyOfRange(32, 64),
|
||||
iv = extendedKey.copyOfRange(64, 80)
|
||||
)
|
||||
}
|
||||
|
||||
class KeyMaterial<Id> (
|
||||
val id: Id,
|
||||
val macKey: ByteArray,
|
||||
val cipherKey: ByteArray,
|
||||
val iv: ByteArray
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.backup
|
||||
|
||||
import org.signal.core.util.Base64
|
||||
|
||||
/**
|
||||
* Safe typing around a mediaId, which is a 15-byte array.
|
||||
*/
|
||||
@JvmInline
|
||||
value class MediaId(val value: ByteArray) {
|
||||
|
||||
init {
|
||||
require(value.size == 15) { "MediaId must be 15 bytes!" }
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return Base64.encodeUrlSafeWithPadding(value)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.signal.libsignal.protocol.logging.Log;
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||
import org.signal.libsignal.usernames.Username;
|
||||
@@ -50,10 +49,15 @@ 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.ArchiveMessageBackupUploadFormResponse;
|
||||
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.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||
@@ -305,11 +309,15 @@ public class PushServiceSocket {
|
||||
private static final String BACKUP_AUTH_CHECK = "/v2/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";
|
||||
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 = "/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 CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
@@ -494,18 +502,14 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation()));
|
||||
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 = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation()));
|
||||
Map<String, String> headers = credentialPresentation.toHeaders();
|
||||
|
||||
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "GET", null, headers, NO_HANDLER);
|
||||
return JsonUtil.fromJson(response, ArchiveGetBackupInfoResponse.class);
|
||||
@@ -529,9 +533,7 @@ public class PushServiceSocket {
|
||||
* @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 = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation()));
|
||||
Map<String, String> headers = credentialPresentation.toHeaders();
|
||||
|
||||
String url = String.format(Locale.US, ARCHIVE_MEDIA_LIST, limit);
|
||||
|
||||
@@ -544,10 +546,39 @@ public class PushServiceSocket {
|
||||
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, NO_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, NO_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 ArchiveMessageBackupUploadFormResponse getArchiveMessageBackupUploadForm(ArchiveCredentialPresentation credentialPresentation) throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation()));
|
||||
headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation()));
|
||||
Map<String, String> headers = credentialPresentation.toHeaders();
|
||||
|
||||
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MESSAGE_UPLOAD_FORM, "GET", null, headers, NO_HANDLER);
|
||||
return JsonUtil.fromJson(response, ArchiveMessageBackupUploadFormResponse.class);
|
||||
@@ -2126,7 +2157,7 @@ public class PushServiceSocket {
|
||||
throw new ServerRejectedException();
|
||||
}
|
||||
|
||||
if (responseCode != 200 && responseCode != 202 && responseCode != 204) {
|
||||
if (responseCode != 200 && responseCode != 202 && responseCode != 204 && responseCode != 207) {
|
||||
throw new NonSuccessfulResponseCodeException(responseCode, "Bad response: " + responseCode + " " + responseMessage);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user