Add initial support for backup and restore of message and media to staging.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Clark
2024-04-12 11:57:34 -04:00
committed by Greyson Parrelli
parent 8617a074ad
commit 689eacd618
71 changed files with 3198 additions and 744 deletions

View File

@@ -6,13 +6,17 @@
package org.whispersystems.signalservice.api;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.FutureTransformers;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.backup.BackupKey;
import org.whispersystems.signalservice.api.backup.MediaId;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
@@ -27,6 +31,7 @@ import org.whispersystems.signalservice.api.push.exceptions.MissingConfiguration
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.internal.push.IdentityCheckRequest;
import org.whispersystems.signalservice.internal.push.IdentityCheckResponse;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
@@ -36,14 +41,18 @@ import org.whispersystems.signalservice.internal.websocket.ResponseMapper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import io.reactivex.rxjava3.core.Single;
@@ -159,10 +168,60 @@ public class SignalServiceMessageReceiver {
throws IOException, InvalidMessageException, MissingConfigurationException {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
socket.retrieveAttachment(pointer.getCdnNumber(), Collections.emptyMap(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), null, 0);
}
/**
* Retrieves an archived media attachment.
*
* @param archivedMediaKeyMaterial Decryption key material for decrypting outer layer of archived media.
* @param readCredentialHeaders Headers to pass to the backup CDN to authorize the download
* @param archiveDestination The download destination for archived attachment. If this file exists, download will resume.
* @param pointer The {@link SignalServiceAttachmentPointer} received in a {@link SignalServiceDataMessage}.
* @param attachmentDestination The download destination for this attachment. If this file exists, it is assumed that this is previously-downloaded content that can be resumed.
* @param listener An optional listener (may be null) to receive callbacks on download progress.
*
* @return An InputStream that streams the plaintext attachment contents.
*/
public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.KeyMaterial<MediaId> archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException
{
if (pointer.getDigest().isEmpty()) {
throw new InvalidMessageException("No attachment digest!");
}
socket.retrieveAttachment(pointer.getCdnNumber(), readCredentialHeaders, pointer.getRemoteId(), archiveDestination, maxSizeBytes, listener);
long originalCipherLength = pointer.getSize()
.filter(s -> s > 0)
.map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s)))
.orElse(0L);
try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMedia(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) {
try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) {
StreamUtil.copy(backupDecrypted, fos);
}
}
return AttachmentCipherInputStream.createForAttachment(attachmentDestination,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0);
}
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, ProgressListener listener) throws MissingConfigurationException, IOException {
socket.retrieveBackup(cdnNumber, headers, cdnPath, destination, 1_000_000_000L, listener);
}
public InputStream retrieveSticker(byte[] packId, byte[] packKey, int stickerId)
throws IOException, InvalidMessageException
{

View File

@@ -841,7 +841,7 @@ public class SignalServiceMessageSender {
Pair<Long, AttachmentDigest> attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes);
return new SignalServiceAttachmentPointer(0,
new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()),
new SignalServiceAttachmentRemoteId.V2(attachmentIdAndDigest.first()),
attachment.getContentType(),
attachmentKey,
Optional.of(Util.toIntExact(attachment.getLength())),
@@ -882,7 +882,7 @@ public class SignalServiceMessageSender {
private SignalServiceAttachmentPointer uploadAttachmentV4(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException {
AttachmentDigest digest = socket.uploadAttachment(attachmentData);
return new SignalServiceAttachmentPointer(attachmentData.getResumableUploadSpec().getCdnNumber(),
new SignalServiceAttachmentRemoteId(attachmentData.getResumableUploadSpec().getCdnKey()),
new SignalServiceAttachmentRemoteId.V4(attachmentData.getResumableUploadSpec().getCdnKey()),
attachment.getContentType(),
attachmentKey,
Optional.of(Util.toIntExact(attachment.getLength())),

View File

@@ -55,6 +55,15 @@ class ArchiveApi(
}
}
fun getCdnReadCredentials(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<GetArchiveCdnCredentialsResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(backupKey, serviceCredential)
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)
pushServiceSocket.getArchiveCdnReadCredentials(presentationData.toArchiveCredentialPresentation())
}
}
/**
* Ensures that you reserve a backupId on the service. This must be done before any other
* backup-related calls. You only need to do it once, but repeated calls are safe.

View File

@@ -16,6 +16,8 @@ data class ArchiveGetBackupInfoResponse(
@JsonProperty
val backupDir: String?,
@JsonProperty
val mediaDir: String?,
@JsonProperty
val backupName: String?,
@JsonProperty
val usedSpace: Long?

View File

@@ -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
/**
* Get response with headers to use to read from archive cdn.
*/
class GetArchiveCdnCredentialsResponse(
@JsonProperty val headers: Map<String, String>
)

View File

@@ -5,6 +5,9 @@
package org.whispersystems.signalservice.api.backup
import org.signal.core.util.Base64
import java.security.MessageDigest
/**
* Safe typing around a backupId, which is a 16-byte array.
*/
@@ -14,4 +17,9 @@ value class BackupId(val value: ByteArray) {
init {
require(value.size == 16) { "BackupId must be 16 bytes!" }
}
/** Encode backup-id for use in a URL/request */
fun encode(): String {
return Base64.encodeUrlSafeWithPadding(MessageDigest.getInstance("SHA-256").digest(value).copyOfRange(0, 16))
}
}

View File

@@ -16,10 +16,14 @@ class BackupKey(val value: ByteArray) {
require(value.size == 32) { "Backup key must be 32 bytes!" }
}
fun deriveSecrets(aci: ACI): KeyMaterial<BackupId> {
val backupId = BackupId(
fun deriveBackupId(aci: ACI): BackupId {
return BackupId(
HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupId".toByteArray(), 16)
)
}
fun deriveSecrets(aci: ACI): KeyMaterial<BackupId> {
val backupId = deriveBackupId(aci)
val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80)
@@ -31,13 +35,15 @@ class BackupKey(val value: ByteArray) {
)
}
fun deriveMediaId(dataHash: ByteArray): MediaId {
return MediaId(HKDF.deriveSecrets(value, dataHash, "Media ID".toByteArray(), 15))
fun deriveMediaId(mediaName: MediaName): MediaId {
return MediaId(HKDF.deriveSecrets(value, mediaName.toByteArray(), "Media ID".toByteArray(), 15))
}
fun deriveMediaSecrets(dataHash: ByteArray): KeyMaterial<MediaId> {
val mediaId = deriveMediaId(dataHash)
fun deriveMediaSecrets(mediaName: MediaName): KeyMaterial<MediaId> {
return deriveMediaSecrets(deriveMediaId(mediaName))
}
fun deriveMediaSecrets(mediaId: MediaId): KeyMaterial<MediaId> {
val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80)
return KeyMaterial(
@@ -53,5 +59,17 @@ class BackupKey(val value: ByteArray) {
val macKey: ByteArray,
val cipherKey: ByteArray,
val iv: ByteArray
)
) {
companion object {
@JvmStatic
fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): KeyMaterial<MediaId> {
return KeyMaterial(
MediaId(id),
keyMac.copyOfRange(32, 64),
keyMac.copyOfRange(0, 32),
iv
)
}
}
}
}

View File

@@ -13,11 +13,14 @@ import org.signal.core.util.Base64
@JvmInline
value class MediaId(val value: ByteArray) {
constructor(mediaId: String) : this(Base64.decode(mediaId))
init {
require(value.size == 15) { "MediaId must be 15 bytes!" }
}
override fun toString(): String {
/** Encode media-id for use in a URL/request */
fun encode(): String {
return Base64.encodeUrlSafeWithPadding(value)
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.backup
import org.signal.core.util.Base64
/**
* Represent a media name for the various types of media that can be archived.
*/
@JvmInline
value class MediaName(val name: String) {
companion object {
fun fromDigest(digest: ByteArray) = MediaName(Base64.encodeWithoutPadding(digest))
fun fromDigestForThumbnail(digest: ByteArray) = MediaName("${Base64.encodeWithoutPadding(digest)}_thumbnail")
}
fun toByteArray(): ByteArray {
return name.toByteArray()
}
}

View File

@@ -10,7 +10,9 @@ import org.signal.libsignal.protocol.InvalidMacException;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice;
import org.signal.libsignal.protocol.incrementalmac.IncrementalMacInputStream;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.whispersystems.signalservice.api.backup.BackupKey;
import org.whispersystems.signalservice.api.backup.MediaId;
import org.whispersystems.signalservice.internal.util.ContentLengthInputStream;
import org.whispersystems.signalservice.internal.util.Util;
@@ -26,6 +28,8 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@@ -47,9 +51,10 @@ public class AttachmentCipherInputStream extends FilterInputStream {
private static final int CIPHER_KEY_SIZE = 32;
private static final int MAC_KEY_SIZE = 32;
private Cipher cipher;
private final Cipher cipher;
private final long totalDataSize;
private boolean done;
private long totalDataSize;
private long totalRead;
private byte[] overflowBuffer;
@@ -102,11 +107,43 @@ public class AttachmentCipherInputStream extends FilterInputStream {
}
}
/**
* Decrypt archived media to it's original attachment encrypted blob.
*/
public static InputStream createForArchivedMedia(BackupKey.KeyMaterial<MediaId> archivedMediaKeyMaterial, File file, long originalCipherTextLength)
throws InvalidMessageException, IOException
{
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(archivedMediaKeyMaterial.getMacKey(), "HmacSHA256"));
if (file.length() <= BLOCK_SIZE + mac.getMacLength()) {
throw new InvalidMessageException("Message shorter than crypto overhead!");
}
try (FileInputStream macVerificationStream = new FileInputStream(file)) {
verifyMac(macVerificationStream, file.length(), mac, null);
}
InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), archivedMediaKeyMaterial.getCipherKey(), file.length() - BLOCK_SIZE - mac.getMacLength());
if (originalCipherTextLength != 0) {
inputStream = new ContentLengthInputStream(inputStream, originalCipherTextLength);
}
return inputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (InvalidMacException e) {
throw new InvalidMessageException(e);
}
}
public static InputStream createForStickerData(byte[] data, byte[] packKey)
throws InvalidMessageException, IOException
{
try {
byte[] combinedKeyMaterial = new HKDFv3().deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
byte[] combinedKeyMaterial = HKDF.deriveSecrets(packKey, "Sticker Pack".getBytes(), 64);
byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(parts[1], "HmacSHA256"));
@@ -159,12 +196,12 @@ public class AttachmentCipherInputStream extends FilterInputStream {
}
@Override
public int read(byte[] buffer) throws IOException {
public int read(@Nonnull byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
public int read(@Nonnull byte[] buffer, int offset, int length) throws IOException {
if (totalRead != totalDataSize) {
return readIncremental(buffer, offset, length);
} else if (!done) {
@@ -256,7 +293,7 @@ public class AttachmentCipherInputStream extends FilterInputStream {
}
}
private static void verifyMac(InputStream inputStream, long length, Mac mac, byte[] theirDigest)
private static void verifyMac(@Nonnull InputStream inputStream, long length, @Nonnull Mac mac, @Nullable byte[] theirDigest)
throws InvalidMacException
{
try {

View File

@@ -1,67 +0,0 @@
package org.whispersystems.signalservice.api.messages;
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.internal.push.AttachmentPointer;
import java.util.Optional;
/**
* Represents a signal service attachment identifier. This can be either a CDN key or a long, but
* not both at once. Attachments V2 used a long as an attachment identifier. This lacks sufficient
* entropy to reduce the likelihood of any two uploads going to the same location within a 30-day
* window. Attachments V3 uses an opaque string as an attachment identifier which provides more
* flexibility in the amount of entropy present.
*/
public final class SignalServiceAttachmentRemoteId {
private final Optional<Long> v2;
private final Optional<String> v3;
public SignalServiceAttachmentRemoteId(long v2) {
this.v2 = Optional.of(v2);
this.v3 = Optional.empty();
}
public SignalServiceAttachmentRemoteId(String v3) {
this.v2 = Optional.empty();
this.v3 = Optional.of(v3);
}
public Optional<Long> getV2() {
return v2;
}
public Optional<String> getV3() {
return v3;
}
@Override
public String toString() {
if (v2.isPresent()) {
return v2.get().toString();
} else {
return v3.get();
}
}
public static SignalServiceAttachmentRemoteId from(AttachmentPointer attachmentPointer) throws InvalidMessageStructureException {
if (attachmentPointer.cdnKey != null) {
return new SignalServiceAttachmentRemoteId(attachmentPointer.cdnKey);
} else if (attachmentPointer.cdnId != null && attachmentPointer.cdnId > 0) {
return new SignalServiceAttachmentRemoteId(attachmentPointer.cdnId);
} else {
throw new InvalidMessageStructureException("AttachmentPointer CDN location not set");
}
}
/**
* Guesses that strings which contain values parseable to {@code long} should use an id-based
* CDN path. Otherwise, use key-based CDN path.
*/
public static SignalServiceAttachmentRemoteId from(String string) {
try {
return new SignalServiceAttachmentRemoteId(Long.parseLong(string));
} catch (NumberFormatException e) {
return new SignalServiceAttachmentRemoteId(string);
}
}
}

View File

@@ -0,0 +1,54 @@
package org.whispersystems.signalservice.api.messages
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.internal.push.AttachmentPointer
/**
* Represents a signal service attachment identifier. This can be either a CDN key or a long, but
* not both at once. Attachments V2 used a long as an attachment identifier. This lacks sufficient
* entropy to reduce the likelihood of any two uploads going to the same location within a 30-day
* window. Attachments V4 (backwards compatible with V3) uses an opaque string as an attachment
* identifier which provides more flexibility in the amount of entropy present.
*/
sealed interface SignalServiceAttachmentRemoteId {
object S3 : SignalServiceAttachmentRemoteId {
override fun toString() = ""
}
data class V2(val cdnId: Long) : SignalServiceAttachmentRemoteId {
override fun toString() = cdnId.toString()
}
data class V4(val cdnKey: String) : SignalServiceAttachmentRemoteId {
override fun toString() = cdnKey
}
data class Backup(val backupDir: String, val mediaDir: String, val mediaId: String) : SignalServiceAttachmentRemoteId {
override fun toString() = mediaId
}
companion object {
@JvmStatic
@Throws(InvalidMessageStructureException::class)
fun from(attachmentPointer: AttachmentPointer): SignalServiceAttachmentRemoteId {
return if (attachmentPointer.cdnKey != null) {
V4(attachmentPointer.cdnKey)
} else if (attachmentPointer.cdnId != null && attachmentPointer.cdnId > 0) {
V2(attachmentPointer.cdnId)
} else {
throw InvalidMessageStructureException("AttachmentPointer CDN location not set")
}
}
/**
* Guesses that strings which contain values parseable to `long` should use an id-based
* CDN path. Otherwise, use key-based CDN path.
*/
@JvmStatic
fun from(string: String): SignalServiceAttachmentRemoteId {
return string.toLongOrNull()?.let { V2(it) } ?: V4(string)
}
}
}

View File

@@ -56,12 +56,12 @@ public final class AttachmentPointerUtil {
builder.incrementalMacChunkSize(attachment.getIncrementalMacChunkSize());
}
if (attachment.getRemoteId().getV2().isPresent()) {
builder.cdnId(attachment.getRemoteId().getV2().get());
if (attachment.getRemoteId() instanceof SignalServiceAttachmentRemoteId.V2) {
builder.cdnId(((SignalServiceAttachmentRemoteId.V2) attachment.getRemoteId()).getCdnId());
}
if (attachment.getRemoteId().getV3().isPresent()) {
builder.cdnKey(attachment.getRemoteId().getV3().get());
if (attachment.getRemoteId() instanceof SignalServiceAttachmentRemoteId.V4) {
builder.cdnKey(((SignalServiceAttachmentRemoteId.V4) attachment.getRemoteId()).getCdnKey());
}
if (attachment.getFileName().isPresent()) {

View File

@@ -58,6 +58,7 @@ 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.UnidentifiedAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
@@ -319,6 +320,7 @@ public class PushServiceSocket {
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/%s";
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
@@ -585,6 +587,16 @@ public class PushServiceSocket {
return JsonUtil.fromJson(response, ArchiveMessageBackupUploadFormResponse.class);
}
/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
*/
public GetArchiveCdnCredentialsResponse getArchiveCdnReadCredentials(@Nonnull ArchiveCredentialPresentation credentialPresentation) throws IOException {
Map<String, String> headers = credentialPresentation.toHeaders();
String response = makeServiceRequestWithoutAuthentication(ARCHIVE_READ_CREDENTIALS, "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(response, GetArchiveCdnCredentialsResponse.class);
}
public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest)
throws IOException
@@ -919,16 +931,27 @@ public class PushServiceSocket {
}, Optional.empty());
}
public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
throws MissingConfigurationException, IOException
{
downloadFromCdn(destination, cdnNumber, headers, cdnPath, maxSizeBytes, listener);
}
public void retrieveAttachment(int cdnNumber, Map<String, String> headers, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener)
throws IOException, MissingConfigurationException
{
final String path;
if (cdnPath.getV2().isPresent()) {
path = String.format(Locale.US, ATTACHMENT_ID_DOWNLOAD_PATH, cdnPath.getV2().get());
if (cdnPath instanceof SignalServiceAttachmentRemoteId.V2) {
path = String.format(Locale.US, ATTACHMENT_ID_DOWNLOAD_PATH, ((SignalServiceAttachmentRemoteId.V2) cdnPath).getCdnId());
} else if (cdnPath instanceof SignalServiceAttachmentRemoteId.V4) {
path = String.format(Locale.US, ATTACHMENT_KEY_DOWNLOAD_PATH, ((SignalServiceAttachmentRemoteId.V4) cdnPath).getCdnKey());
} else if (cdnPath instanceof SignalServiceAttachmentRemoteId.Backup) {
SignalServiceAttachmentRemoteId.Backup backupCdnId = (SignalServiceAttachmentRemoteId.Backup) cdnPath;
path = String.format(Locale.US, ARCHIVE_MEDIA_DOWNLOAD_PATH, backupCdnId.getBackupDir(), backupCdnId.getMediaDir(), backupCdnId.getMediaId());
} else {
path = String.format(Locale.US, ATTACHMENT_KEY_DOWNLOAD_PATH, cdnPath.getV3().get());
throw new IllegalArgumentException("Invalid cdnPath type: " + cdnPath.getClass().getSimpleName());
}
downloadFromCdn(destination, cdnNumber, path, maxSizeBytes, listener);
downloadFromCdn(destination, cdnNumber, headers, path, maxSizeBytes, listener);
}
public byte[] retrieveSticker(byte[] packId, int stickerId)
@@ -937,7 +960,7 @@ public class PushServiceSocket {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
downloadFromCdn(output, 0, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
downloadFromCdn(output, 0, 0, Collections.emptyMap(), String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null);
} catch (MissingConfigurationException e) {
throw new AssertionError(e);
}
@@ -951,7 +974,7 @@ public class PushServiceSocket {
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
downloadFromCdn(output, 0, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
downloadFromCdn(output, 0, 0, Collections.emptyMap(), String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null);
} catch (MissingConfigurationException e) {
throw new AssertionError(e);
}
@@ -1029,7 +1052,7 @@ public class PushServiceSocket {
throws IOException
{
try {
downloadFromCdn(destination, 0, path, maxSizeBytes, null);
downloadFromCdn(destination, 0, Collections.emptyMap(), path, maxSizeBytes, null);
} catch (MissingConfigurationException e) {
throw new AssertionError(e);
}
@@ -1577,15 +1600,15 @@ public class PushServiceSocket {
}
}
private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
private void downloadFromCdn(File destination, int cdnNumber, Map<String, String> headers, String path, long maxSizeBytes, ProgressListener listener)
throws IOException, MissingConfigurationException
{
try (FileOutputStream outputStream = new FileOutputStream(destination, true)) {
downloadFromCdn(outputStream, destination.length(), cdnNumber, path, maxSizeBytes, listener);
downloadFromCdn(outputStream, destination.length(), cdnNumber, headers, path, maxSizeBytes, listener);
}
}
private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener)
private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumber, Map<String, String> headers, String path, long maxSizeBytes, ProgressListener listener)
throws PushNetworkException, NonSuccessfulResponseCodeException, MissingConfigurationException {
ConnectionHolder[] cdnNumberClients = cdnClientsMap.get(cdnNumber);
if (cdnNumberClients == null) {
@@ -1604,6 +1627,10 @@ public class PushServiceSocket {
request.addHeader("Host", connectionHolder.getHostHeader().get());
}
for (Map.Entry<String, String> header : headers.entrySet()) {
request.addHeader(header.getKey(), header.getValue());
}
if (offset > 0) {
Log.i(TAG, "Starting download from CDN with offset " + offset);
request.addHeader("Range", "bytes=" + offset + "-");

View File

@@ -300,7 +300,7 @@ public class WebSocketConnection extends WebSocketListener {
OutgoingRequest listener = outgoingRequests.remove(message.response.id);
if (listener != null) {
listener.onSuccess(new WebsocketResponse(message.response.status,
new String(message.response.body.toByteArray()),
message.response.body == null ? "" : new String(message.response.body.toByteArray()),
message.response.headers,
!credentialsProvider.isPresent()));
if (message.response.status >= 400) {