From 5c3ea712fef48799f10ae53275bfb06503306145 Mon Sep 17 00:00:00 2001 From: Clark Date: Tue, 21 May 2024 21:00:36 -0400 Subject: [PATCH] Add streaming video support for attachment files. --- .../conversation/ConversationItem.java | 18 ++++++++- .../securesms/video/exo/PartDataSource.java | 37 +++++++++++++------ .../signalservice/api/backup/BackupKey.kt | 4 ++ .../crypto/AttachmentCipherInputStream.java | 37 +++++++++++++++++++ 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 41bc0447cf..b9aa94ee57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy; import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; +import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; @@ -2489,7 +2490,22 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } if (MediaUtil.isInstantVideoSupported(slide)) { final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment(); - if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED) { + if (databaseAttachment.transferState == AttachmentTable.TRANSFER_RESTORE_OFFLOADED) { + final AttachmentId attachmentId = databaseAttachment.attachmentId; + final JobManager jobManager = ApplicationDependencies.getJobManager(); + final String queue = RestoreAttachmentJob.constructQueueString(attachmentId); + setup(v, slide); + jobManager.add(new RestoreAttachmentJob(messageRecord.getId(), + attachmentId, + true, + false, + RestoreAttachmentJob.RestoreMode.ORIGINAL)); + jobManager.addListener(queue, (job, jobState) -> { + if (jobState.isComplete()) { + cleanup(); + } + }); + } else if (databaseAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_STARTED) { final AttachmentId attachmentId = databaseAttachment.attachmentId; final JobManager jobManager = ApplicationDependencies.getJobManager(); final String queue = AttachmentDownloadJob.constructQueueString(attachmentId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java index 11a79cf9ca..bf40405b49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/video/exo/PartDataSource.java @@ -16,9 +16,14 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.PartUriParser; import org.signal.core.util.Base64; +import org.whispersystems.signalservice.api.backup.BackupKey; +import org.whispersystems.signalservice.api.backup.MediaId; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; +import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; +import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import java.io.EOFException; import java.io.File; @@ -62,20 +67,30 @@ class PartDataSource implements DataSource { if (inProgress && !hasData && hasIncrementalDigest && attachmentKey != null) { final byte[] decode = Base64.decode(attachmentKey); - final File transferFile = attachmentDatabase.getOrCreateTransferFile(attachment.attachmentId); - try { - this.inputStream = AttachmentCipherInputStream.createForAttachment(transferFile, attachment.size, decode, attachment.remoteDigest, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize); + if (attachment.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && attachment.archiveMediaId != null) { + final File archiveFile = attachmentDatabase.getOrCreateArchiveTransferFile(attachment.attachmentId); + try { + BackupKey.MediaKeyMaterial mediaKeyMaterial = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey().deriveMediaSecretsFromMediaId(attachment.archiveMediaId); + long originalCipherLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size)); - long skipped = 0; - while (skipped < dataSpec.position) { - skipped += this.inputStream.read(); + this.inputStream = AttachmentCipherInputStream.createStreamingForArchivedAttachment(mediaKeyMaterial, archiveFile, originalCipherLength, attachment.size, attachment.remoteDigest, decode, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize); + } catch (InvalidMessageException e) { + throw new IOException("Error decrypting attachment stream!", e); + } + } else { + final File transferFile = attachmentDatabase.getOrCreateTransferFile(attachment.attachmentId); + try { + this.inputStream = AttachmentCipherInputStream.createForAttachment(transferFile, attachment.size, decode, attachment.remoteDigest, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize); + } catch (InvalidMessageException e) { + throw new IOException("Error decrypting attachment stream!", e); } - - Log.d(TAG, "Successfully loaded partial attachment file."); - - } catch (InvalidMessageException e) { - throw new IOException("Error decrypting attachment stream!", e); } + long skipped = 0; + while (skipped < dataSpec.position) { + skipped += this.inputStream.read(); + } + + Log.d(TAG, "Successfully loaded partial attachment file."); } else if (!inProgress || hasData) { this.inputStream = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position); diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt index 17f8b876db..abca72a70c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt @@ -42,6 +42,10 @@ class BackupKey(val value: ByteArray) { return deriveMediaSecrets(deriveMediaId(mediaName)) } + fun deriveMediaSecretsFromMediaId(base64MediaId: String): MediaKeyMaterial { + return deriveMediaSecrets(MediaId(base64MediaId)) + } + fun deriveThumbnailTransitKey(thumbnailMediaName: MediaName): ByteArray { return HKDF.deriveSecrets(value, deriveMediaId(thumbnailMediaName).value, "20240513_Signal_Backups_EncryptThumbnail".toByteArray(), 64) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java index 1e3e797a77..8ff7501ef9 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java @@ -138,6 +138,43 @@ public class AttachmentCipherInputStream extends FilterInputStream { return inputStream; } + public static InputStream createStreamingForArchivedAttachment(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest, int incrementalMacChunkSize) + throws InvalidMessageException, IOException + { + final InputStream archiveStream = createForArchivedMedia(archivedMediaKeyMaterial, file, originalCipherTextLength); + + byte[][] parts = Util.split(combinedKeyMaterial, CIPHER_KEY_SIZE, MAC_KEY_SIZE); + Mac mac = initMac(parts[1]); + + if (originalCipherTextLength <= BLOCK_SIZE + mac.getMacLength()) { + throw new InvalidMessageException("Message shorter than crypto overhead!"); + } + + if (digest == null) { + throw new InvalidMessageException("Missing digest!"); + } + + final InputStream wrappedStream; + wrappedStream = new IncrementalMacInputStream( + new IncrementalMacAdditionalValidationsInputStream( + archiveStream, + file.length(), + mac, + digest + ), + parts[1], + ChunkSizeChoice.everyNthByte(incrementalMacChunkSize), + incrementalDigest); + + InputStream inputStream = new AttachmentCipherInputStream(wrappedStream, parts[0], file.length() - BLOCK_SIZE - mac.getMacLength()); + + if (plaintextLength != 0) { + inputStream = new ContentLengthInputStream(inputStream, plaintextLength); + } + + return inputStream; + } + public static InputStream createForStickerData(byte[] data, byte[] packKey) throws InvalidMessageException, IOException {