From ac9e5505ae4062e67cd20c65c014a09300cc8b07 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 30 Aug 2024 12:13:54 -0400 Subject: [PATCH] Save IV on attachment download. --- .../securesms/database/AttachmentTable.kt | 3 +- .../securesms/database/MediaTable.kt | 1 + .../securesms/jobs/AttachmentDownloadJob.kt | 6 +- .../securesms/jobs/RestoreAttachmentJob.kt | 10 ++-- .../jobs/RestoreAttachmentThumbnailJob.kt | 6 +- .../api/SignalServiceMessageReceiver.java | 57 +++++++++++++------ .../attachment/AttachmentDownloadResult.kt | 16 ++++++ 7 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentDownloadResult.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index 46c28cc8af..c85d16c79d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -913,7 +913,7 @@ class AttachmentTable( * that the content of the attachment will never change. */ @Throws(MmsException::class) - fun finalizeAttachmentAfterDownload(mmsId: Long, attachmentId: AttachmentId, inputStream: InputStream) { + fun finalizeAttachmentAfterDownload(mmsId: Long, attachmentId: AttachmentId, inputStream: InputStream, iv: ByteArray?) { Log.i(TAG, "[finalizeAttachmentAfterDownload] Finalizing downloaded data for $attachmentId. (MessageId: $mmsId, $attachmentId)") val existingPlaceholder: DatabaseAttachment = getAttachment(attachmentId) ?: throw MmsException("No attachment found for id: $attachmentId") @@ -962,6 +962,7 @@ class AttachmentTable( values.put(TRANSFER_FILE, null as String?) values.put(TRANSFORM_PROPERTIES, TransformProperties.forSkipTransform().serialize()) values.put(ARCHIVE_TRANSFER_FILE, null as String?) + values.put(REMOTE_IV, iv) db.update(TABLE_NAME) .values(values) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index c50b0d343a..ec0181d6f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -32,6 +32,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD ${AttachmentTable.TABLE_NAME}.${AttachmentTable.CDN_NUMBER}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_LOCATION}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_KEY}, + ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_IV}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.REMOTE_DIGEST}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.FAST_PREFLIGHT_ID}, ${AttachmentTable.TABLE_NAME}.${AttachmentTable.VOICE_NOTE}, diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt index 4e904982ef..9042c34c29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt @@ -264,7 +264,7 @@ class AttachmentDownloadJob private constructor( } } - val stream = if (useArchiveCdn) { + val downloadResult = if (useArchiveCdn) { archiveFile = SignalDatabase.attachments.getOrCreateArchiveTransferFile(attachmentId) val cdnCredentials = BackupRepository.getCdnReadCredentials(attachment.archiveCdn).successOrThrow().headers @@ -289,7 +289,7 @@ class AttachmentDownloadJob private constructor( ) } - SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, stream) + SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, downloadResult.dataStream, downloadResult.iv) } catch (e: RangeException) { val transferFile = archiveFile ?: attachmentFile Log.w(TAG, "Range exception, file size " + transferFile.length(), e) @@ -415,7 +415,7 @@ class AttachmentDownloadJob private constructor( if (body.contentLength() > RemoteConfig.maxAttachmentReceiveSizeBytes) { throw MmsException("Attachment too large, failing download") } - SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, (body.source() as Source).buffer().inputStream()) + SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, (body.source() as Source).buffer().inputStream(), iv = null) } } } catch (e: MmsException) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index 3a4dfdc1f5..901e8da136 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -240,7 +240,7 @@ class RestoreAttachmentJob private constructor( } } - val stream = if (useArchiveCdn) { + val downloadResult = if (useArchiveCdn) { archiveFile = SignalDatabase.attachments.getOrCreateArchiveTransferFile(attachmentId) val cdnCredentials = BackupRepository.getCdnReadCredentials(attachment.archiveCdn).successOrThrow().headers @@ -265,7 +265,7 @@ class RestoreAttachmentJob private constructor( ) } - SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, stream) + SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, downloadResult.dataStream, downloadResult.iv) } catch (e: RangeException) { val transferFile = archiveFile ?: attachmentFile Log.w(TAG, "Range exception, file size " + transferFile.length(), e) @@ -459,7 +459,7 @@ class RestoreAttachmentJob private constructor( val pointer = createThumbnailPointer(attachment) Log.w(TAG, "Downloading thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}") - val stream = messageReceiver + val downloadResult = messageReceiver .retrieveArchivedAttachment( SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()), cdnCredentials, @@ -467,11 +467,11 @@ class RestoreAttachmentJob private constructor( pointer, thumbnailFile, maxThumbnailSize, - true, // TODO [backup] don't ignore + true, progressListener ) - SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, stream, thumbnailTransferFile) + SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, downloadResult.dataStream, thumbnailTransferFile) } private fun markFailed(messageId: Long, attachmentId: AttachmentId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt index 4c67984e7d..fae3983731 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt @@ -216,7 +216,7 @@ class RestoreAttachmentThumbnailJob private constructor( val pointer = createThumbnailPointer(attachment) Log.w(TAG, "Downloading thumbnail for $attachmentId") - val stream = messageReceiver + val downloadResult = messageReceiver .retrieveArchivedAttachment( SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()), cdnCredentials, @@ -224,11 +224,11 @@ class RestoreAttachmentThumbnailJob private constructor( pointer, thumbnailFile, maxThumbnailSize, - true, // TODO [backup] don't ignore + true, progressListener ) - SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, stream, thumbnailTransferFile) + SignalDatabase.attachments.finalizeAttachmentThumbnailAfterDownload(attachmentId, attachment.archiveMediaId!!, downloadResult.dataStream, thumbnailTransferFile) } private fun markFailed(messageId: Long, attachmentId: AttachmentId) { 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 b86c3472ae..be61d6a3d0 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 @@ -13,6 +13,7 @@ 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.attachment.AttachmentDownloadResult; import org.whispersystems.signalservice.api.backup.BackupKey; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; @@ -93,7 +94,7 @@ public class SignalServiceMessageReceiver { */ public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes) throws IOException, InvalidMessageException, MissingConfigurationException { - return retrieveAttachment(pointer, destination, maxSizeBytes, null); + return retrieveAttachment(pointer, destination, maxSizeBytes, null).getDataStream(); } public ListenableFuture retrieveProfile(SignalServiceAddress address, @@ -164,12 +165,21 @@ public class SignalServiceMessageReceiver { * @throws IOException * @throws InvalidMessageException */ - public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener) + public AttachmentDownloadResult retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, InvalidMessageException, MissingConfigurationException { if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!"); 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); + + byte[] iv = new byte[16]; + try (InputStream tempStream = new FileInputStream(destination)) { + StreamUtil.readFully(tempStream, iv); + } + + return new AttachmentDownloadResult( + AttachmentCipherInputStream.createForAttachment(destination, pointer.getSize().orElse(0), pointer.getKey(), pointer.getDigest().get(), null, 0), + iv + ); } /** @@ -184,14 +194,14 @@ public class SignalServiceMessageReceiver { * * @return An InputStream that streams the plaintext attachment contents. */ - public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, - @Nonnull Map readCredentialHeaders, - @Nonnull File archiveDestination, - @Nonnull SignalServiceAttachmentPointer pointer, - @Nonnull File attachmentDestination, - long maxSizeBytes, - boolean ignoreDigest, - @Nullable ProgressListener listener) + public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, + @Nonnull Map readCredentialHeaders, + @Nonnull File archiveDestination, + @Nonnull SignalServiceAttachmentPointer pointer, + @Nonnull File attachmentDestination, + long maxSizeBytes, + boolean ignoreDigest, + @Nullable ProgressListener listener) throws IOException, InvalidMessageException, MissingConfigurationException { if (!ignoreDigest && pointer.getDigest().isEmpty()) { @@ -205,19 +215,30 @@ public class SignalServiceMessageReceiver { .map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s))) .orElse(0L); + // There's two layers of encryption -- one from the backup, and one from the attachment. This only strips the outermost backup encryption layer. try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMedia(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) { try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) { + // TODO [backup] I don't think we should be doing the full copy here. This is basically doing the entire download inline in this single line. StreamUtil.copy(backupDecrypted, fos); } } - return AttachmentCipherInputStream.createForAttachment(attachmentDestination, - pointer.getSize().orElse(0), - pointer.getKey(), - ignoreDigest ? null : pointer.getDigest().get(), - null, - 0, - ignoreDigest); + byte[] iv = new byte[16]; + try (InputStream tempStream = new FileInputStream(attachmentDestination)) { + StreamUtil.readFully(tempStream, iv); + } + + InputStream dataStream = AttachmentCipherInputStream.createForAttachment( + attachmentDestination, + pointer.getSize().orElse(0), + pointer.getKey(), + ignoreDigest ? null : pointer.getDigest().get(), + null, + 0, + ignoreDigest + ); + + return new AttachmentDownloadResult(dataStream, iv); } public void retrieveBackup(int cdnNumber, Map headers, String cdnPath, File destination, ProgressListener listener) throws MissingConfigurationException, IOException { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentDownloadResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentDownloadResult.kt new file mode 100644 index 0000000000..a7d0a17c6f --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/attachment/AttachmentDownloadResult.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.attachment + +import java.io.InputStream + +/** + * Holds the result of an attachment download. + */ +class AttachmentDownloadResult( + val dataStream: InputStream, + val iv: ByteArray +)