From 37a35e8f7012b494bb3d2a46aafc2d6de8a8245a Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Sun, 5 Apr 2020 17:32:06 -0700 Subject: [PATCH] Add initial support for send/receive on CDN2. --- app/build.gradle | 2 + .../securesms/attachments/Attachment.java | 9 +- .../attachments/DatabaseAttachment.java | 6 +- .../MmsNotificationAttachment.java | 2 +- .../attachments/PointerAttachment.java | 10 +- .../attachments/TombstoneAttachment.java | 2 +- .../securesms/attachments/UriAttachment.java | 4 +- .../database/AttachmentDatabase.java | 25 ++- .../securesms/database/GroupDatabase.java | 4 +- .../securesms/database/MediaDatabase.java | 1 + .../securesms/database/MmsDatabase.java | 1 + .../securesms/database/MmsSmsDatabase.java | 25 +-- .../database/helpers/SQLCipherOpenHelper.java | 7 +- .../dependencies/ApplicationDependencies.java | 8 +- .../ApplicationDependencyProvider.java | 2 + .../groups/GroupV1MessageProcessor.java | 2 +- .../securesms/jobs/AttachmentDownloadJob.java | 15 +- .../securesms/jobs/AvatarDownloadJob.java | 8 +- .../securesms/jobs/PushSendJob.java | 11 +- .../push/SignalServiceNetworkAccess.java | 15 ++ .../securesms/util/FeatureFlags.java | 12 +- .../api/SignalServiceMessagePipe.java | 27 ++- .../api/SignalServiceMessageReceiver.java | 2 +- .../api/SignalServiceMessageSender.java | 88 +++++++-- .../SignalServiceAttachmentPointer.java | 49 ++--- .../SignalServiceAttachmentRemoteId.java | 69 +++++++ .../api/messages/SignalServiceContent.java | 16 +- .../SignalServiceConfiguration.java | 7 + ...java => AttachmentV2UploadAttributes.java} | 4 +- .../push/AttachmentV3UploadAttributes.java | 38 ++++ .../internal/push/PushServiceSocket.java | 176 +++++++++++++++--- .../src/main/proto/SignalService.proto | 7 +- 32 files changed, 510 insertions(+), 144 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentRemoteId.java rename libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/{AttachmentUploadAttributes.java => AttachmentV2UploadAttributes.java} (92%) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV3UploadAttributes.java diff --git a/app/build.gradle b/app/build.gradle index 4bc5742484..5aee07c72d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -115,6 +115,7 @@ android { buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\"" buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" + buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" @@ -194,6 +195,7 @@ android { buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\"" buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\"" + buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\"" diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java index b709a7b532..990c9b9de0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Attachment.java @@ -19,6 +19,8 @@ public abstract class Attachment { @Nullable private final String fileName; + private final int cdnNumber; + @Nullable private final String location; @@ -53,7 +55,7 @@ public abstract class Attachment { private final TransformProperties transformProperties; public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName, - @Nullable String location, @Nullable String key, @Nullable String relay, + int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @@ -63,6 +65,7 @@ public abstract class Attachment { this.transferState = transferState; this.size = size; this.fileName = fileName; + this.cdnNumber = cdnNumber; this.location = location; this.key = key; this.relay = relay; @@ -108,6 +111,10 @@ public abstract class Attachment { return contentType; } + public int getCdnNumber() { + return cdnNumber; + } + @Nullable public String getLocation() { return location; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index c62d82d827..651f161914 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.attachments; import android.net.Uri; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.stickers.StickerLocator; @@ -24,14 +22,14 @@ public class DatabaseAttachment extends Attachment { public DatabaseAttachment(AttachmentId attachmentId, long mmsId, boolean hasData, boolean hasThumbnail, String contentType, int transferProgress, long size, - String fileName, String location, String key, String relay, + String fileName, int cdnNumber, String location, String key, String relay, byte[] digest, String fastPreflightId, boolean voiceNote, int width, int height, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties, int displayOrder, long uploadTimestamp) { - super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties); + super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index bb38d33bc5..28c3055e82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null); + super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null); } @Nullable diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java index 4b9c58b7b8..e6053ebf3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -18,13 +18,13 @@ import java.util.List; public class PointerAttachment extends Attachment { private PointerAttachment(@NonNull String contentType, int transferState, long size, - @Nullable String fileName, @NonNull String location, + @Nullable String fileName, int cdnNumber, @NonNull String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash) { - super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null); + super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null); } @Nullable @@ -93,7 +93,8 @@ public class PointerAttachment extends Attachment { AttachmentDatabase.TRANSFER_PROGRESS_PENDING, pointer.get().asPointer().getSize().or(0), pointer.get().asPointer().getFileName().orNull(), - String.valueOf(pointer.get().asPointer().getId()), + pointer.get().asPointer().getCdnNumber(), + pointer.get().asPointer().getRemoteId().toString(), encodedKey, null, pointer.get().asPointer().getDigest().orNull(), fastPreflightId, @@ -114,7 +115,8 @@ public class PointerAttachment extends Attachment { AttachmentDatabase.TRANSFER_PROGRESS_PENDING, thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0, pointer.getFileName(), - String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0), + thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0, + thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0", thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, null, thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java index cab3c04cac..4c6f91b079 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.java @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase; public class TombstoneAttachment extends Attachment { public TombstoneAttachment(@NonNull String contentType, boolean quote) { - super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null); + super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java index a9d29cf58c..391d85363c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.attachments; import android.net.Uri; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.blurhash.BlurHash; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties; import org.thoughtcrime.securesms.stickers.StickerLocator; @@ -27,7 +27,7 @@ public class UriAttachment extends Attachment { boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties) { - super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties); + super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties); this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 5aded42db8..80443293ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -124,6 +124,7 @@ public class AttachmentDatabase extends Database { static final String TRANSFORM_PROPERTIES = "transform_properties"; static final String DISPLAY_ORDER = "display_order"; static final String UPLOAD_TIMESTAMP = "upload_timestamp"; + static final String CDN_NUMBER = "cdn_number"; public static final String DIRECTORY = "parts"; @@ -139,13 +140,14 @@ public class AttachmentDatabase extends Database { private static final String[] PROJECTION = new String[] {ROW_ID, MMS_ID, CONTENT_TYPE, NAME, CONTENT_DISPOSITION, - CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, - SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, - UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, - QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT, - CAPTION, STICKER_PACK_ID, STICKER_PACK_KEY, STICKER_ID, - DATA_HASH, BLUR_HASH, TRANSFORM_PROPERTIES, TRANSFER_FILE, - DISPLAY_ORDER, UPLOAD_TIMESTAMP }; + CDN_NUMBER, CONTENT_LOCATION, DATA, THUMBNAIL, + TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL, + THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST, + FAST_PREFLIGHT_ID, VOICE_NOTE, QUOTE, DATA_RANDOM, + THUMBNAIL_RANDOM, WIDTH, HEIGHT, CAPTION, STICKER_PACK_ID, + STICKER_PACK_KEY, STICKER_ID, DATA_HASH, BLUR_HASH, + TRANSFORM_PROPERTIES, TRANSFER_FILE, DISPLAY_ORDER, + UPLOAD_TIMESTAMP }; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + @@ -184,7 +186,8 @@ public class AttachmentDatabase extends Database { TRANSFORM_PROPERTIES + " TEXT DEFAULT NULL, " + TRANSFER_FILE + " TEXT DEFAULT NULL, " + DISPLAY_ORDER + " INTEGER DEFAULT 0, " + - UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0);"; + UPLOAD_TIMESTAMP + " INTEGER DEFAULT 0, " + + CDN_NUMBER + " INTEGER DEFAULT 0);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -532,6 +535,7 @@ public class AttachmentDatabase extends Database { } values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(CDN_NUMBER, 0); values.put(CONTENT_LOCATION, (String)null); values.put(CONTENT_DISPOSITION, (String)null); values.put(DIGEST, (byte[])null); @@ -584,6 +588,7 @@ public class AttachmentDatabase extends Database { contentValues.put(DATA_RANDOM, sourceDataInfo.random); contentValues.put(TRANSFER_STATE, sourceAttachment.getTransferState()); + contentValues.put(CDN_NUMBER, sourceAttachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, sourceAttachment.getLocation()); contentValues.put(DIGEST, sourceAttachment.getDigest()); contentValues.put(CONTENT_DISPOSITION, sourceAttachment.getKey()); @@ -630,6 +635,7 @@ public class AttachmentDatabase extends Database { ContentValues values = new ContentValues(); values.put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE); + values.put(CDN_NUMBER, attachment.getCdnNumber()); values.put(CONTENT_LOCATION, attachment.getLocation()); values.put(DIGEST, attachment.getDigest()); values.put(CONTENT_DISPOSITION, attachment.getKey()); @@ -1105,6 +1111,7 @@ public class AttachmentDatabase extends Database { object.getInt(TRANSFER_STATE), object.getLong(SIZE), object.getString(FILE_NAME), + object.getInt(CDN_NUMBER), object.getString(CONTENT_LOCATION), object.getString(CONTENT_DISPOSITION), object.getString(NAME), @@ -1138,6 +1145,7 @@ public class AttachmentDatabase extends Database { cursor.getInt(cursor.getColumnIndexOrThrow(TRANSFER_STATE)), cursor.getLong(cursor.getColumnIndexOrThrow(SIZE)), cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)), + cursor.getInt(cursor.getColumnIndexOrThrow(CDN_NUMBER)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_LOCATION)), cursor.getString(cursor.getColumnIndexOrThrow(CONTENT_DISPOSITION)), cursor.getString(cursor.getColumnIndexOrThrow(NAME)), @@ -1197,6 +1205,7 @@ public class AttachmentDatabase extends Database { contentValues.put(CONTENT_TYPE, template.getContentType()); contentValues.put(TRANSFER_STATE, attachment.getTransferState()); contentValues.put(UNIQUE_ID, uniqueId); + contentValues.put(CDN_NUMBER, useTemplateUpload ? template.getCdnNumber() : attachment.getCdnNumber()); contentValues.put(CONTENT_LOCATION, useTemplateUpload ? template.getLocation() : attachment.getLocation()); contentValues.put(DIGEST, useTemplateUpload ? template.getDigest() : attachment.getDigest()); contentValues.put(CONTENT_DISPOSITION, useTemplateUpload ? template.getKey() : attachment.getKey()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 186901fa81..de207146ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -282,7 +282,7 @@ public final class GroupDatabase extends Database { contentValues.put(MEMBERS, RecipientId.toSerializedList(members)); if (avatar != null) { - contentValues.put(AVATAR_ID, avatar.getId()); + contentValues.put(AVATAR_ID, avatar.getRemoteId().getV2().get()); contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); @@ -326,7 +326,7 @@ public final class GroupDatabase extends Database { if (title != null) contentValues.put(TITLE, title); if (avatar != null) { - contentValues.put(AVATAR_ID, avatar.getId()); + contentValues.put(AVATAR_ID, avatar.getRemoteId().getV2().get()); contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType()); contentValues.put(AVATAR_KEY, avatar.getKey()); contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java index cbc098dc00..e71feffb06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -31,6 +31,7 @@ public class MediaDatabase extends Database { + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FILE_NAME + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DIGEST + ", " diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 17e7bb0bf7..66c53931f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -198,6 +198,7 @@ public class MmsDatabase extends MessagingDatabase { "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + "," + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + "," + diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index c3ea5b1c84..194da63628 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -346,6 +346,7 @@ public class MmsSmsDatabase extends Database { "'" + AttachmentDatabase.DATA + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.DATA + ", " + "'" + AttachmentDatabase.THUMBNAIL + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL + ", " + "'" + AttachmentDatabase.CONTENT_TYPE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", " + + "'" + AttachmentDatabase.CDN_NUMBER + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CDN_NUMBER + ", " + "'" + AttachmentDatabase.CONTENT_LOCATION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_LOCATION + ", " + "'" + AttachmentDatabase.FAST_PREFLIGHT_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.FAST_PREFLIGHT_ID + ", " + "'" + AttachmentDatabase.VOICE_NOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.VOICE_NOTE + ", " + @@ -455,30 +456,6 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.STATUS); mmsColumnsPresent.add(MmsDatabase.UNIDENTIFIED); mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE); - - mmsColumnsPresent.add(AttachmentDatabase.ROW_ID); - mmsColumnsPresent.add(AttachmentDatabase.UNIQUE_ID); - mmsColumnsPresent.add(AttachmentDatabase.MMS_ID); - mmsColumnsPresent.add(AttachmentDatabase.SIZE); - mmsColumnsPresent.add(AttachmentDatabase.FILE_NAME); - mmsColumnsPresent.add(AttachmentDatabase.DATA); - mmsColumnsPresent.add(AttachmentDatabase.THUMBNAIL); - mmsColumnsPresent.add(AttachmentDatabase.CONTENT_TYPE); - mmsColumnsPresent.add(AttachmentDatabase.CONTENT_LOCATION); - mmsColumnsPresent.add(AttachmentDatabase.DIGEST); - mmsColumnsPresent.add(AttachmentDatabase.FAST_PREFLIGHT_ID); - mmsColumnsPresent.add(AttachmentDatabase.VOICE_NOTE); - mmsColumnsPresent.add(AttachmentDatabase.WIDTH); - mmsColumnsPresent.add(AttachmentDatabase.HEIGHT); - mmsColumnsPresent.add(AttachmentDatabase.QUOTE); - mmsColumnsPresent.add(AttachmentDatabase.STICKER_PACK_ID); - mmsColumnsPresent.add(AttachmentDatabase.STICKER_PACK_KEY); - mmsColumnsPresent.add(AttachmentDatabase.STICKER_ID); - mmsColumnsPresent.add(AttachmentDatabase.CAPTION); - mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION); - mmsColumnsPresent.add(AttachmentDatabase.NAME); - mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE); - mmsColumnsPresent.add(AttachmentDatabase.ATTACHMENT_JSON_ALIAS); mmsColumnsPresent.add(MmsDatabase.QUOTE_ID); mmsColumnsPresent.add(MmsDatabase.QUOTE_AUTHOR); mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 4df76ecb0f..771876ac8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -126,8 +126,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int AVATAR_LOCATION_MIGRATION = 54; private static final int GROUPS_V2 = 55; private static final int ATTACHMENT_UPLOAD_TIMESTAMP = 56; + private static final int ATTACHMENT_CDN_NUMBER = 57; - private static final int DATABASE_VERSION = 56; + private static final int DATABASE_VERSION = ATTACHMENT_CDN_NUMBER; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -863,6 +864,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE part ADD COLUMN upload_timestamp DEFAULT 0"); } + if (oldVersion < ATTACHMENT_CDN_NUMBER) { + db.execSQL("ALTER TABLE part ADD COLUMN cdn_number INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index f9c4082f5a..f27a056664 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.service.IncomingMessageObserver; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FrameRateTracker; import org.thoughtcrime.securesms.util.IasKeyStore; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -83,8 +84,11 @@ public class ApplicationDependencies { if (messageSender == null) { messageSender = provider.provideSignalServiceMessageSender(); } else { - messageSender.setMessagePipe(IncomingMessageObserver.getPipe(), IncomingMessageObserver.getUnidentifiedPipe()); - messageSender.setIsMultiDevice(TextSecurePreferences.isMultiDevice(application)); + messageSender.update( + IncomingMessageObserver.getPipe(), + IncomingMessageObserver.getUnidentifiedPipe(), + TextSecurePreferences.isMultiDevice(application), + FeatureFlags.attachmentsV3()); } return messageSender; diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 3f4ab575c9..deac052f4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.service.IncomingMessageObserver; import org.thoughtcrime.securesms.util.AlarmSleepTimer; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FrameRateTracker; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.util.guava.Optional; @@ -79,6 +80,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr new SignalProtocolStoreImpl(context), BuildConfig.SIGNAL_AGENT, TextSecurePreferences.isMultiDevice(context), + FeatureFlags.attachmentsV3(), Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()), Optional.of(new SecurityEventListener(context)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index 150e33f943..d189c407bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -271,7 +271,7 @@ public final class GroupV1MessageProcessor { if (group.getAvatar().isPresent() && group.getAvatar().get().isPointer()) { builder.setAvatar(AttachmentPointer.newBuilder() - .setId(group.getAvatar().get().asPointer().getId()) + .setCdnId(group.getAvatar().get().asPointer().getRemoteId().getV2().get()) .setKey(ByteString.copyFrom(group.getAvatar().get().asPointer().getKey())) .setContentType(group.getAvatar().get().getContentType())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index 04f06d69a4..913648f1d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -1,8 +1,9 @@ package org.thoughtcrime.securesms.jobs; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.attachments.Attachment; @@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; @@ -182,13 +184,8 @@ public class AttachmentDownloadJob extends BaseJob { } try { - long id = Long.parseLong(attachment.getLocation()); - byte[] key = Base64.decode(attachment.getKey()); - String relay = null; - - if (TextUtils.isEmpty(attachment.getRelay())) { - relay = attachment.getRelay(); - } + final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation()); + final byte[] key = Base64.decode(attachment.getKey()); if (attachment.getDigest() != null) { Log.i(TAG, "Downloading attachment with digest: " + Hex.toString(attachment.getDigest())); @@ -196,7 +193,7 @@ public class AttachmentDownloadJob extends BaseJob { Log.i(TAG, "Downloading attachment with no digest..."); } - return new SignalServiceAttachmentPointer(id, null, key, + return new SignalServiceAttachmentPointer(attachment.getCdnNumber(), remoteId, null, key, Optional.of(Util.toIntExact(attachment.getSize())), Optional.absent(), 0, 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java index 9d72ac58cf..0131a9ffe5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AvatarDownloadJob.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.jobs; -import android.graphics.Bitmap; - import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -13,14 +11,12 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.profiles.AvatarHelper; -import org.thoughtcrime.securesms.util.BitmapDecodingException; -import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.Hex; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; @@ -90,7 +86,7 @@ public class AvatarDownloadJob extends BaseJob { attachment.deleteOnExit(); SignalServiceMessageReceiver receiver = ApplicationDependencies.getSignalServiceMessageReceiver(); - SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent(), System.currentTimeMillis()); + SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId(avatarId), contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), Optional.absent(), System.currentTimeMillis()); InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index c38183bae2..388c2833bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.jobs; import android.content.Context; import android.graphics.Bitmap; +import android.text.TextUtils; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import android.text.TextUtils; import com.annimon.stream.Stream; @@ -43,6 +44,7 @@ import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview; @@ -184,10 +186,11 @@ public abstract class PushSendJob extends SendJob { } try { - long id = Long.parseLong(attachment.getLocation()); - byte[] key = Base64.decode(attachment.getKey()); + final SignalServiceAttachmentRemoteId remoteId = SignalServiceAttachmentRemoteId.from(attachment.getLocation()); + final byte[] key = Base64.decode(attachment.getKey()); - return new SignalServiceAttachmentPointer(id, + return new SignalServiceAttachmentPointer(attachment.getCdnNumber(), + remoteId, attachment.getContentType(), key, Optional.of(Util.toIntExact(attachment.getSize())), diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index 85dc9b45a5..3be38d674e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -121,6 +121,16 @@ public class SignalServiceNetworkAccess { final SignalCdnUrl omanGoogleCdn = new SignalCdnUrl("https://www.google.com.om/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalCdnUrl qatarGoogleCdn = new SignalCdnUrl("https://www.google.com.qa/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl baseGoogleCdn2 = new SignalCdnUrl("https://www.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl baseAndroidCdn2 = new SignalCdnUrl("https://android.clients.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); + final SignalCdnUrl mapsOneAndroidCdn2 = new SignalCdnUrl("https://clients3.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); + final SignalCdnUrl mapsTwoAndroidCdn2 = new SignalCdnUrl("https://clients4.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); + final SignalCdnUrl mailAndroidCdn2 = new SignalCdnUrl("https://inbox.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl egyptGoogleCdn2 = new SignalCdnUrl("https://www.google.com.eg/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl uaeGoogleCdn2 = new SignalCdnUrl("https://www.google.ae/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl omanGoogleCdn2 = new SignalCdnUrl("https://www.google.com.om/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalCdnUrl qatarGoogleCdn2 = new SignalCdnUrl("https://www.google.com.qa/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); + final SignalContactDiscoveryUrl baseGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalContactDiscoveryUrl baseAndroidDiscovery = new SignalContactDiscoveryUrl("https://android.clients.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); final SignalContactDiscoveryUrl mapsOneAndroidDiscovery = new SignalContactDiscoveryUrl("https://clients3.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); @@ -165,6 +175,7 @@ public class SignalServiceNetworkAccess { this.censorshipConfiguration = new HashMap() {{ put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {egyptGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn, mailAndroidCdn}, + new SignalCdnUrl[] {egyptGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2, mailAndroidCdn2}, new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, @@ -174,6 +185,7 @@ public class SignalServiceNetworkAccess { put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, + new SignalCdnUrl[] {uaeGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}, new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, @@ -183,6 +195,7 @@ public class SignalServiceNetworkAccess { put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, + new SignalCdnUrl[] {omanGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}, new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, @@ -193,6 +206,7 @@ public class SignalServiceNetworkAccess { put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, new SignalCdnUrl[] {qatarGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, + new SignalCdnUrl[] {qatarGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}, new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, @@ -203,6 +217,7 @@ public class SignalServiceNetworkAccess { this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))}, new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))}, + new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, new SignalServiceTrustStore(context))}, new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}, new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) }, new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 5c5b4194e7..13fb108dc2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -54,6 +54,7 @@ public final class FeatureFlags { private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch"; private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone"; private static final String STORAGE_SERVICE = "android.storageService.2"; + private static final String ATTACHMENTS_V3 = "android.attachmentsV3"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -66,7 +67,8 @@ public final class FeatureFlags { PINS_MEGAPHONE_KILL_SWITCH, PROFILE_NAMES_MEGAPHONE, MESSAGE_REQUESTS, - STORAGE_SERVICE + STORAGE_SERVICE, + ATTACHMENTS_V3 ); /** @@ -88,7 +90,8 @@ public final class FeatureFlags { */ private static final Set HOT_SWAPPABLE = Sets.newHashSet( PINS_MEGAPHONE_KILL_SWITCH, - STORAGE_SERVICE + STORAGE_SERVICE, + ATTACHMENTS_V3 ); /** @@ -213,6 +216,11 @@ public final class FeatureFlags { return getValue(STORAGE_SERVICE, false); } + /** Whether or not we use the attachments v3 form. */ + public static boolean attachmentsV3() { + return getValue(ATTACHMENTS_V3, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java index 7a133fe40f..c279db6e53 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java @@ -26,7 +26,8 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.CredentialsProvider; -import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes; +import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; +import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList; import org.whispersystems.signalservice.internal.push.SendMessageResponse; import org.whispersystems.signalservice.internal.util.JsonUtil; @@ -219,7 +220,7 @@ public class SignalServiceMessagePipe { } } - public AttachmentUploadAttributes getAttachmentUploadAttributes() throws IOException { + public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws IOException { try { WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() .setId(new SecureRandom().nextLong()) @@ -233,7 +234,27 @@ public class SignalServiceMessagePipe { throw new IOException("Non-successful response: " + response.first()); } - return JsonUtil.fromJson(response.second(), AttachmentUploadAttributes.class); + return JsonUtil.fromJson(response.second(), AttachmentV2UploadAttributes.class); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IOException(e); + } + } + + public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws IOException { + try { + WebSocketRequestMessage requestMessage = WebSocketRequestMessage.newBuilder() + .setId(new SecureRandom().nextLong()) + .setVerb("GET") + .setPath("/v3/attachments/form/upload") + .build(); + + Pair response = websocket.sendRequest(requestMessage).get(10, TimeUnit.SECONDS); + + if (response.first() < 200 || response.first() >= 300) { + throw new IOException("Non-successful response: " + response.first()); + } + + return JsonUtil.fromJson(response.second(), AttachmentV3UploadAttributes.class); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new IOException(e); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index df445f1b9a..ce44fe5702 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -178,7 +178,7 @@ public class SignalServiceMessageReceiver { { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); - socket.retrieveAttachment(pointer.getId(), destination, maxSizeBytes, listener); + socket.retrieveAttachment(pointer.getCdnNumber(), pointer.getRemoteId(), destination, maxSizeBytes, listener); return AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get()); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index b403c11636..2a1711e052 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -25,6 +25,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -50,12 +51,14 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; -import org.whispersystems.signalservice.internal.push.AttachmentUploadAttributes; +import org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes; +import org.whispersystems.signalservice.internal.push.AttachmentV3UploadAttributes; import org.whispersystems.signalservice.internal.push.MismatchedDevices; import org.whispersystems.signalservice.internal.push.OutgoingPushMessage; import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList; @@ -110,6 +113,7 @@ public class SignalServiceMessageSender { private final AtomicReference> pipe; private final AtomicReference> unidentifiedPipe; private final AtomicBoolean isMultiDevice; + private final AtomicBoolean attachmentsV3; /** * Construct a SignalServiceMessageSender. @@ -127,12 +131,13 @@ public class SignalServiceMessageSender { SignalProtocolStore store, String signalAgent, boolean isMultiDevice, + boolean attachmentsV3, Optional pipe, Optional unidentifiedPipe, Optional eventListener, ClientZkProfileOperations clientZkProfileOperations) { - this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations); + this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, attachmentsV3, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations); } public SignalServiceMessageSender(SignalServiceConfiguration urls, @@ -140,6 +145,7 @@ public class SignalServiceMessageSender { SignalProtocolStore store, String signalAgent, boolean isMultiDevice, + boolean attachmentsV3, Optional pipe, Optional unidentifiedPipe, Optional eventListener, @@ -151,6 +157,7 @@ public class SignalServiceMessageSender { this.pipe = new AtomicReference<>(pipe); this.unidentifiedPipe = new AtomicReference<>(unidentifiedPipe); this.isMultiDevice = new AtomicBoolean(isMultiDevice); + this.attachmentsV3 = new AtomicBoolean(attachmentsV3); this.eventListener = eventListener; } @@ -336,13 +343,11 @@ public class SignalServiceMessageSender { socket.cancelInFlightRequests(); } - public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) { + public void update(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe, boolean isMultiDevice, boolean attachmentsV3) { this.pipe.set(Optional.fromNullable(pipe)); this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe)); - } - - public void setIsMultiDevice(boolean isMultiDevice) { this.isMultiDevice.set(isMultiDevice); + this.attachmentsV3.set(attachmentsV3); } public SignalServiceAttachmentPointer uploadAttachment(SignalServiceAttachmentStream attachment) throws IOException { @@ -357,26 +362,35 @@ public class SignalServiceMessageSender { attachment.getListener(), attachment.getCancelationSignal()); - AttachmentUploadAttributes uploadAttributes = null; - Optional localPipe = pipe.get(); + if (attachmentsV3.get()) { + return uploadAttachmentV3(attachment, attachmentKey, attachmentData); + } else { + return uploadAttachmentV2(attachment, attachmentKey, attachmentData); + } + } + + private SignalServiceAttachmentPointer uploadAttachmentV2(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws NonSuccessfulResponseCodeException, PushNetworkException { + AttachmentV2UploadAttributes v2UploadAttributes = null; + Optional localPipe = pipe.get(); if (localPipe.isPresent()) { Log.d(TAG, "Using pipe to retrieve attachment upload attributes..."); try { - uploadAttributes = localPipe.get().getAttachmentUploadAttributes(); + v2UploadAttributes = localPipe.get().getAttachmentV2UploadAttributes(); } catch (IOException e) { Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back..."); } } - if (uploadAttributes == null) { + if (v2UploadAttributes == null) { Log.d(TAG, "Not using pipe to retrieve attachment upload attributes..."); - uploadAttributes = socket.getAttachmentUploadAttributes(); + v2UploadAttributes = socket.getAttachmentV2UploadAttributes(); } - Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, uploadAttributes); + Pair attachmentIdAndDigest = socket.uploadAttachment(attachmentData, v2UploadAttributes); - return new SignalServiceAttachmentPointer(attachmentIdAndDigest.first(), + return new SignalServiceAttachmentPointer(0, + new SignalServiceAttachmentRemoteId(attachmentIdAndDigest.first()), attachment.getContentType(), attachmentKey, Optional.of(Util.toIntExact(attachment.getLength())), @@ -390,6 +404,41 @@ public class SignalServiceMessageSender { attachment.getUploadTimestamp()); } + private SignalServiceAttachmentPointer uploadAttachmentV3(SignalServiceAttachmentStream attachment, byte[] attachmentKey, PushAttachmentData attachmentData) throws IOException { + AttachmentV3UploadAttributes v3UploadAttributes = null; + Optional localPipe = pipe.get(); + + if (localPipe.isPresent()) { + Log.d(TAG, "Using pipe to retrieve attachment upload attributes..."); + try { + v3UploadAttributes = localPipe.get().getAttachmentV3UploadAttributes(); + } catch (IOException e) { + Log.w(TAG, "Failed to retrieve attachment upload attributes using pipe. Falling back..."); + } + } + + if (v3UploadAttributes == null) { + Log.d(TAG, "Not using pipe to retrieve attachment upload attributes..."); + v3UploadAttributes = socket.getAttachmentV3UploadAttributes(); + } + + byte[] digest = socket.uploadAttachment(attachmentData, v3UploadAttributes); + return new SignalServiceAttachmentPointer(v3UploadAttributes.getCdn(), + new SignalServiceAttachmentRemoteId(v3UploadAttributes.getKey()), + attachment.getContentType(), + attachmentKey, + Optional.of(Util.toIntExact(attachment.getLength())), + attachment.getPreview(), + attachment.getWidth(), + attachment.getHeight(), + Optional.of(digest), + attachment.getFileName(), + attachment.getVoiceNote(), + attachment.getCaption(), + attachment.getBlurHash(), + attachment.getUploadTimestamp()); + } + private void sendMessage(VerifiedMessage message, Optional unidentifiedAccess) throws IOException, UntrustedIdentityException @@ -1205,12 +1254,20 @@ public class SignalServiceMessageSender { private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentPointer attachment) { AttachmentPointer.Builder builder = AttachmentPointer.newBuilder() + .setCdnNumber(attachment.getCdnNumber()) .setContentType(attachment.getContentType()) - .setId(attachment.getId()) .setKey(ByteString.copyFrom(attachment.getKey())) .setDigest(ByteString.copyFrom(attachment.getDigest().get())) .setSize(attachment.getSize().get()); + if (attachment.getRemoteId().getV2().isPresent()) { + builder.setCdnId(attachment.getRemoteId().getV2().get()); + } + + if (attachment.getRemoteId().getV3().isPresent()) { + builder.setCdnKey(attachment.getRemoteId().getV3().get()); + } + if (attachment.getFileName().isPresent()) { builder.setFileName(attachment.getFileName().get()); } @@ -1245,8 +1302,7 @@ public class SignalServiceMessageSender { private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment) throws IOException { - SignalServiceAttachmentPointer pointer = uploadAttachment(attachment); - return createAttachmentPointer(pointer); + return createAttachmentPointer(uploadAttachment(attachment)); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java index afdd6c1d9c..23fcfbe99a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentPointer.java @@ -18,28 +18,31 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; */ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { - private final long id; - private final byte[] key; - private final Optional size; - private final Optional preview; - private final Optional digest; - private final Optional fileName; - private final boolean voiceNote; - private final int width; - private final int height; - private final Optional caption; - private final Optional blurHash; - private final long uploadTimestamp; + private final int cdnNumber; + private final SignalServiceAttachmentRemoteId remoteId; + private final byte[] key; + private final Optional size; + private final Optional preview; + private final Optional digest; + private final Optional fileName; + private final boolean voiceNote; + private final int width; + private final int height; + private final Optional caption; + private final Optional blurHash; + private final long uploadTimestamp; - public SignalServiceAttachmentPointer(long id, String contentType, byte[] key, - Optional size, Optional preview, - int width, int height, - Optional digest, Optional fileName, - boolean voiceNote, Optional caption, - Optional blurHash, long uploadTimestamp) + public SignalServiceAttachmentPointer(int cdnNumber, SignalServiceAttachmentRemoteId remoteId, + String contentType, byte[] key, + Optional size, Optional preview, int width, + int height, Optional digest, + Optional fileName, boolean voiceNote, + Optional caption, Optional blurHash, + long uploadTimestamp) { super(contentType); - this.id = id; + this.cdnNumber = cdnNumber; + this.remoteId = remoteId; this.key = key; this.size = size; this.preview = preview; @@ -53,8 +56,12 @@ public class SignalServiceAttachmentPointer extends SignalServiceAttachment { this.uploadTimestamp = uploadTimestamp; } - public long getId() { - return id; + public int getCdnNumber() { + return cdnNumber; + } + + public SignalServiceAttachmentRemoteId getRemoteId() { + return remoteId; } public byte[] getKey() { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentRemoteId.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentRemoteId.java new file mode 100644 index 0000000000..3525129a35 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceAttachmentRemoteId.java @@ -0,0 +1,69 @@ +package org.whispersystems.signalservice.api.messages; + +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.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 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 v2; + private final Optional v3; + + public SignalServiceAttachmentRemoteId(long v2) { + this.v2 = Optional.of(v2); + this.v3 = Optional.absent(); + } + + public SignalServiceAttachmentRemoteId(String v3) { + this.v2 = Optional.absent(); + this.v3 = Optional.of(v3); + } + + public Optional getV2() { + return v2; + } + + public Optional 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 ProtocolInvalidMessageException { + switch (attachmentPointer.getAttachmentIdentifierCase()) { + case CDNID: + return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnId()); + case CDNKEY: + return new SignalServiceAttachmentRemoteId(attachmentPointer.getCdnKey()); + case ATTACHMENTIDENTIFIER_NOT_SET: + throw new ProtocolInvalidMessageException(new InvalidMessageException("AttachmentPointer CDN location not set"), null, 0); + } + return null; + } + + /** + * 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); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index ba4c13c6ae..91d3ab1024 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -562,7 +562,7 @@ public final class SignalServiceContent { Optional.absent()); } - private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) { + private static SignalServiceDataMessage.Quote createQuote(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (!content.hasQuote()) return null; List attachments = new LinkedList<>(); @@ -586,7 +586,7 @@ public final class SignalServiceContent { } } - private static List createPreviews(SignalServiceProtos.DataMessage content) { + private static List createPreviews(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (content.getPreviewCount() <= 0) return null; List results = new LinkedList<>(); @@ -606,7 +606,7 @@ public final class SignalServiceContent { return results; } - private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) { + private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (!content.hasSticker() || !content.getSticker().hasPackId() || !content.getSticker().hasPackKey() || @@ -641,7 +641,7 @@ public final class SignalServiceContent { reaction.getTargetSentTimestamp()); } - private static List createSharedContacts(SignalServiceProtos.DataMessage content) { + private static List createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException { if (content.getContactCount() <= 0) return null; List results = new LinkedList<>(); @@ -736,8 +736,9 @@ public final class SignalServiceContent { return results; } - private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) { - return new SignalServiceAttachmentPointer(pointer.getId(), + private static SignalServiceAttachmentPointer createAttachmentPointer(SignalServiceProtos.AttachmentPointer pointer) throws ProtocolInvalidMessageException { + return new SignalServiceAttachmentPointer(pointer.getCdnNumber(), + SignalServiceAttachmentRemoteId.from(pointer), pointer.getContentType(), pointer.getKey().toByteArray(), pointer.hasSize() ? Optional.of(pointer.getSize()) : Optional.absent(), @@ -795,7 +796,8 @@ public final class SignalServiceContent { if (content.getGroup().hasAvatar()) { SignalServiceProtos.AttachmentPointer pointer = content.getGroup().getAvatar(); - avatar = new SignalServiceAttachmentPointer(pointer.getId(), + avatar = new SignalServiceAttachmentPointer(pointer.getCdnNumber(), + SignalServiceAttachmentRemoteId.from(pointer), pointer.getContentType(), pointer.getKey().toByteArray(), Optional.of(pointer.getSize()), diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java index 5d92de9104..22fb03794e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java @@ -11,6 +11,7 @@ public final class SignalServiceConfiguration { private final SignalServiceUrl[] signalServiceUrls; private final SignalCdnUrl[] signalCdnUrls; + private final SignalCdnUrl[] signalCdn2Urls; private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls; private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls; private final SignalStorageUrl[] signalStorageUrls; @@ -20,6 +21,7 @@ public final class SignalServiceConfiguration { public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, SignalCdnUrl[] signalCdnUrls, + SignalCdnUrl[] signalCdn2Urls, SignalContactDiscoveryUrl[] signalContactDiscoveryUrls, SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls, SignalStorageUrl[] signalStorageUrls, @@ -29,6 +31,7 @@ public final class SignalServiceConfiguration { { this.signalServiceUrls = signalServiceUrls; this.signalCdnUrls = signalCdnUrls; + this.signalCdn2Urls = signalCdn2Urls; this.signalContactDiscoveryUrls = signalContactDiscoveryUrls; this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls; this.signalStorageUrls = signalStorageUrls; @@ -45,6 +48,10 @@ public final class SignalServiceConfiguration { return signalCdnUrls; } + public SignalCdnUrl[] getSignalCdn2Urls() { + return signalCdn2Urls; + } + public SignalContactDiscoveryUrl[] getSignalContactDiscoveryUrls() { return signalContactDiscoveryUrls; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentUploadAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV2UploadAttributes.java similarity index 92% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentUploadAttributes.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV2UploadAttributes.java index 46eadd264a..f4fbecde0e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentUploadAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV2UploadAttributes.java @@ -3,7 +3,7 @@ package org.whispersystems.signalservice.internal.push; import com.fasterxml.jackson.annotation.JsonProperty; -public class AttachmentUploadAttributes { +public class AttachmentV2UploadAttributes { @JsonProperty private String url; @@ -34,7 +34,7 @@ public class AttachmentUploadAttributes { @JsonProperty private String attachmentIdString; - public AttachmentUploadAttributes() {} + public AttachmentV2UploadAttributes() {} public String getUrl() { return url; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV3UploadAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV3UploadAttributes.java new file mode 100644 index 0000000000..d5f7f7d7f3 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/AttachmentV3UploadAttributes.java @@ -0,0 +1,38 @@ +package org.whispersystems.signalservice.internal.push; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public final class AttachmentV3UploadAttributes { + @JsonProperty + private int cdn; + + @JsonProperty + private String key; + + @JsonProperty + private Map headers; + + @JsonProperty + private String signedUploadLocation; + + public AttachmentV3UploadAttributes() { + } + + public int getCdn() { + return cdn; + } + + public String getKey() { + return key; + } + + public Map getHeaders() { + return headers; + } + + public String getSignedUploadLocation() { + return signedUploadLocation; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 96c1fd0b67..93edb8c62d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -35,6 +35,7 @@ import org.whispersystems.signalservice.FeatureFlags; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -114,6 +115,7 @@ import okhttp3.Call; import okhttp3.ConnectionSpec; import okhttp3.Credentials; import okhttp3.Dns; +import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.MultipartBody; @@ -159,7 +161,8 @@ public class PushServiceSocket { private static final String MESSAGE_PATH = "/v1/messages/%s"; private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d"; private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s"; - private static final String ATTACHMENT_PATH = "/v2/attachments/form/upload"; + private static final String ATTACHMENT_V2_PATH = "/v2/attachments/form/upload"; + private static final String ATTACHMENT_V3_PATH = "/v3/attachments/form/upload"; private static final String PROFILE_PATH = "/v1/profile/%s"; private static final String PROFILE_USERNAME_PATH = "/v1/profile/username/%s"; @@ -167,14 +170,15 @@ public class PushServiceSocket { private static final String SENDER_CERTIFICATE_LEGACY_PATH = "/v1/certificate/delivery"; private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery?includeUuid=true"; - private static final String KBS_AUTH_PATH = "/v1/backup/auth"; + private static final String KBS_AUTH_PATH = "/v1/backup/auth"; - private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d"; - private static final String ATTACHMENT_UPLOAD_PATH = "attachments/"; - private static final String AVATAR_UPLOAD_PATH = ""; + 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"; - private static final String STICKER_PATH = "stickers/%s/full/%d"; + private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto"; + private static final String STICKER_PATH = "stickers/%s/full/%d"; private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d"; private static final String GROUPSV2_GROUP = "/v1/groups/"; @@ -189,6 +193,7 @@ public class PushServiceSocket { private final ServiceConnectionHolder[] serviceClients; private final ConnectionHolder[] cdnClients; + private final ConnectionHolder[] cdn2Clients; private final ConnectionHolder[] contactDiscoveryClients; private final ConnectionHolder[] keyBackupServiceClients; private final ConnectionHolder[] storageClients; @@ -207,6 +212,7 @@ public class PushServiceSocket { this.signalAgent = signalAgent; this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.cdn2Clients = createConnectionHolders(configuration.getSignalCdn2Urls(), configuration.getNetworkInterceptors(), configuration.getDns()); this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); @@ -516,17 +522,23 @@ public class PushServiceSocket { makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); } - public void retrieveAttachment(long attachmentId, File destination, long maxSizeBytes, ProgressListener listener) + public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener) throws NonSuccessfulResponseCodeException, PushNetworkException { - downloadFromCdn(destination, String.format(Locale.US, ATTACHMENT_DOWNLOAD_PATH, attachmentId), maxSizeBytes, listener); + final String path; + if (cdnPath.getV2().isPresent()) { + path = String.format(Locale.US, ATTACHMENT_ID_DOWNLOAD_PATH, cdnPath.getV2().get()); + } else { + path = String.format(Locale.US, ATTACHMENT_KEY_DOWNLOAD_PATH, cdnPath.getV3().get()); + } + downloadFromCdn(destination, cdnNumber, path, maxSizeBytes, listener); } public void retrieveSticker(File destination, byte[] packId, int stickerId) throws NonSuccessfulResponseCodeException, PushNetworkException { String hexPackId = Hex.toStringCondensed(packId); - downloadFromCdn(destination, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null); + downloadFromCdn(destination, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null); } public byte[] retrieveSticker(byte[] packId, int stickerId) @@ -535,7 +547,7 @@ public class PushServiceSocket { String hexPackId = Hex.toStringCondensed(packId); ByteArrayOutputStream output = new ByteArrayOutputStream(); - downloadFromCdn(output, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null); + downloadFromCdn(output, 0, 0, String.format(Locale.US, STICKER_PATH, hexPackId, stickerId), 1024 * 1024, null); return output.toByteArray(); } @@ -546,7 +558,7 @@ public class PushServiceSocket { String hexPackId = Hex.toStringCondensed(packId); ByteArrayOutputStream output = new ByteArrayOutputStream(); - downloadFromCdn(output, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null); + downloadFromCdn(output, 0, 0, String.format(STICKER_MANIFEST_PATH, hexPackId), 1024 * 1024, null); return output.toByteArray(); } @@ -611,7 +623,7 @@ public class PushServiceSocket { public void retrieveProfileAvatar(String path, File destination, long maxSizeBytes) throws NonSuccessfulResponseCodeException, PushNetworkException { - downloadFromCdn(destination, path, maxSizeBytes, null); + downloadFromCdn(destination, 0, path, maxSizeBytes, null); } public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException { @@ -867,10 +879,20 @@ public class PushServiceSocket { } } - public AttachmentUploadAttributes getAttachmentUploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException { - String response = makeServiceRequest(ATTACHMENT_PATH, "GET", null); + public AttachmentV2UploadAttributes getAttachmentV2UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException { + String response = makeServiceRequest(ATTACHMENT_V2_PATH, "GET", null); try { - return JsonUtil.fromJson(response, AttachmentUploadAttributes.class); + return JsonUtil.fromJson(response, AttachmentV2UploadAttributes.class); + } catch (IOException e) { + Log.w(TAG, e); + throw new NonSuccessfulResponseCodeException("Unable to parse entity"); + } + } + + public AttachmentV3UploadAttributes getAttachmentV3UploadAttributes() throws NonSuccessfulResponseCodeException, PushNetworkException { + String response = makeServiceRequest(ATTACHMENT_V3_PATH, "GET", null); + try { + return JsonUtil.fromJson(response, AttachmentV3UploadAttributes.class); } catch (IOException e) { Log.w(TAG, e); throw new NonSuccessfulResponseCodeException("Unable to parse entity"); @@ -890,7 +912,7 @@ public class PushServiceSocket { null, null); } - public Pair uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes) + public Pair uploadAttachment(PushAttachmentData attachment, AttachmentV2UploadAttributes uploadAttributes) throws PushNetworkException, NonSuccessfulResponseCodeException { long id = Long.parseLong(uploadAttributes.getAttachmentId()); @@ -905,20 +927,31 @@ public class PushServiceSocket { return new Pair<>(id, digest); } - private void downloadFromCdn(File destination, String path, long maxSizeBytes, ProgressListener listener) + public byte[] uploadAttachment(PushAttachmentData attachment, AttachmentV3UploadAttributes uploadAttributes) throws IOException { + String resumableUploadUrl = getResumableUploadUrl(uploadAttributes.getSignedUploadLocation(), uploadAttributes.getHeaders()); + return uploadToCdn2(resumableUploadUrl, + attachment.getData(), + "application/octet-stream", + attachment.getDataSize(), + attachment.getOutputStreamFactory(), + attachment.getListener(), + attachment.getCancelationSignal()); + } + + private void downloadFromCdn(File destination, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { try (FileOutputStream outputStream = new FileOutputStream(destination, true)) { - downloadFromCdn(outputStream, destination.length(), path, maxSizeBytes, listener); + downloadFromCdn(outputStream, destination.length(), cdnNumber, path, maxSizeBytes, listener); } catch (IOException e) { throw new PushNetworkException(e); } } - private void downloadFromCdn(OutputStream outputStream, long offset, String path, long maxSizeBytes, ProgressListener listener) + private void downloadFromCdn(OutputStream outputStream, long offset, int cdnNumber, String path, long maxSizeBytes, ProgressListener listener) throws PushNetworkException, NonSuccessfulResponseCodeException { - ConnectionHolder connectionHolder = getRandom(cdnClients, random); + ConnectionHolder connectionHolder = getRandom(cdnNumber == 2 ? cdn2Clients : cdnClients, random); OkHttpClient okHttpClient = connectionHolder.getClient() .newBuilder() .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) @@ -1046,6 +1079,107 @@ public class PushServiceSocket { } } + private String getResumableUploadUrl(String signedUrl, Map headers) throws IOException { + ConnectionHolder connectionHolder = getRandom(cdn2Clients, random); + OkHttpClient okHttpClient = connectionHolder.getClient() + .newBuilder() + .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .build(); + final HttpUrl endpointUrl = HttpUrl.get(connectionHolder.url); + final HttpUrl signedHttpUrl; + try { + signedHttpUrl = HttpUrl.get(signedUrl); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Server returned a malformed signed url: " + signedUrl); + throw new IOException("Server returned a malformed signed url", e); + } + + final HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme(endpointUrl.scheme()) + .host(endpointUrl.host()) + .port(endpointUrl.port()) + .encodedPath(endpointUrl.encodedPath()) + .addEncodedPathSegments(signedHttpUrl.encodedPath().substring(1)) + .encodedQuery(signedHttpUrl.encodedQuery()) + .encodedFragment(signedHttpUrl.encodedFragment()); + + Request.Builder request = new Request.Builder().url(urlBuilder.build()) + .post(RequestBody.create(null, "")); + for (Map.Entry header : headers.entrySet()) { + request.header(header.getKey(), header.getValue()); + } + + if (connectionHolder.getHostHeader().isPresent()) { + request.header("host", connectionHolder.getHostHeader().get()); + } + + Call call = okHttpClient.newCall(request.build()); + + synchronized (connections) { + connections.add(call); + } + + try { + Response response; + + try { + response = call.execute(); + } catch (IOException e) { + throw new PushNetworkException(e); + } + + if (response.isSuccessful()) { + return response.header("location"); + } else { + throw new NonSuccessfulResponseCodeException("Response: " + response); + } + } finally { + synchronized (connections) { + connections.remove(call); + } + } + } + + private byte[] uploadToCdn2(String resumableUrl, InputStream data, String contentType, long length, OutputStreamFactory outputStreamFactory, ProgressListener progressListener, CancelationSignal cancelationSignal) throws IOException { + ConnectionHolder connectionHolder = getRandom(cdn2Clients, random); + OkHttpClient okHttpClient = connectionHolder.getClient() + .newBuilder() + .connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS) + .build(); + + DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener, cancelationSignal); + Request.Builder request = new Request.Builder().url(resumableUrl) + .put(file); + + if (connectionHolder.getHostHeader().isPresent()) { + request.header("host", connectionHolder.getHostHeader().get()); + } + + Call call = okHttpClient.newCall(request.build()); + + synchronized (connections) { + connections.add(call); + } + + try { + Response response; + + try { + response = call.execute(); + } catch (IOException e) { + throw new PushNetworkException(e); + } + + if (response.isSuccessful()) return file.getTransmittedDigest(); + else throw new NonSuccessfulResponseCodeException("Response: " + response); + } finally { + synchronized (connections) { + connections.remove(call); + } + } + } + private String makeServiceRequest(String urlFragment, String method, String jsonBody) throws NonSuccessfulResponseCodeException, PushNetworkException { diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 672a9bcc05..702535557d 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -377,7 +377,10 @@ message AttachmentPointer { VOICE_MESSAGE = 1; } - optional fixed64 id = 1; + oneof attachment_identifier { + fixed64 cdnId = 1; + string cdnKey = 15; + } optional string contentType = 2; optional bytes key = 3; optional uint32 size = 4; @@ -390,6 +393,8 @@ message AttachmentPointer { optional string caption = 11; optional string blurHash = 12; optional uint64 uploadTimestamp = 13; + optional uint32 cdnNumber = 14; + // Next ID: 16 } message GroupContext {