mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-27 13:13:43 +00:00
Allow normal attachments to be validated with plaintextHashes.
This commit is contained in:
committed by
Cody Henthorne
parent
607b83d65b
commit
ec5452744d
@@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
import org.whispersystems.signalservice.api.backup.MediaName;
|
||||
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
|
||||
|
||||
import java.io.InterruptedIOException;
|
||||
|
||||
@@ -19,6 +19,7 @@ import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Base64.decodeBase64OrThrow
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.EventTimer
|
||||
@@ -37,7 +38,7 @@ import org.signal.core.util.getForeignKeyViolations
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.requireIntOrNull
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.stream.NonClosingOutputStream
|
||||
import org.signal.core.util.urlEncode
|
||||
import org.signal.core.util.withinTransaction
|
||||
@@ -1290,11 +1291,11 @@ object BackupRepository {
|
||||
return initBackupAndFetchAuth()
|
||||
.then { credential ->
|
||||
SignalNetwork.archive.getMessageBackupUploadForm(SignalStore.account.requireAci(), credential.messageBackupAccess)
|
||||
.also { Log.i(TAG, "UploadFormResult: $it") }
|
||||
.also { Log.i(TAG, "UploadFormResult: ${it::class.simpleName}") }
|
||||
}
|
||||
.then { form ->
|
||||
SignalNetwork.archive.getBackupResumableUploadUrl(form)
|
||||
.also { Log.i(TAG, "ResumableUploadUrlResult: $it") }
|
||||
.also { Log.i(TAG, "ResumableUploadUrlResult: ${it::class.simpleName}") }
|
||||
.map { ResumableMessagesBackupUploadSpec(attachmentUploadForm = form, resumableUri = it) }
|
||||
}
|
||||
}
|
||||
@@ -1307,7 +1308,7 @@ object BackupRepository {
|
||||
): NetworkResult<Unit> {
|
||||
val (form, resumableUploadUrl) = resumableSpec
|
||||
return SignalNetwork.archive.uploadBackupFile(form, resumableUploadUrl, backupStream, backupStreamLength, progressListener)
|
||||
.also { Log.i(TAG, "UploadBackupFileResult: $it") }
|
||||
.also { Log.i(TAG, "UploadBackupFileResult: ${it::class.simpleName}") }
|
||||
}
|
||||
|
||||
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): NetworkResult<Unit> {
|
||||
@@ -1395,7 +1396,7 @@ object BackupRepository {
|
||||
.map { response ->
|
||||
SignalDatabase.attachments.setArchiveCdn(attachmentId = attachment.attachmentId, archiveCdn = response.cdn)
|
||||
}
|
||||
.also { Log.i(TAG, "archiveMediaResult: $it") }
|
||||
.also { Log.i(TAG, "archiveMediaResult: ${it::class.simpleName}") }
|
||||
}
|
||||
|
||||
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
|
||||
@@ -1421,7 +1422,7 @@ object BackupRepository {
|
||||
mediaToDelete = mediaToDelete
|
||||
)
|
||||
}
|
||||
.also { Log.i(TAG, "deleteAbandonedMediaObjectsResult: $it") }
|
||||
.also { Log.i(TAG, "deleteAbandonedMediaObjectsResult: ${it::class.simpleName}") }
|
||||
}
|
||||
|
||||
fun deleteBackup(): NetworkResult<Unit> {
|
||||
@@ -1477,7 +1478,7 @@ object BackupRepository {
|
||||
.map {
|
||||
SignalDatabase.attachments.clearAllArchiveData()
|
||||
}
|
||||
.also { Log.i(TAG, "debugDeleteAllArchivedMediaResult: $it") }
|
||||
.also { Log.i(TAG, "debugDeleteAllArchivedMediaResult: ${it::class.simpleName}") }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1512,7 +1513,7 @@ object BackupRepository {
|
||||
credentialStore.cdnReadCredentials = it.result
|
||||
}
|
||||
}
|
||||
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
|
||||
.also { Log.i(TAG, "getCdnReadCredentialsResult: ${it::class.simpleName}") }
|
||||
}
|
||||
|
||||
fun restoreBackupTier(aci: ACI): MessageBackupTier? {
|
||||
@@ -1965,8 +1966,8 @@ class ArchiveMediaItemIterator(private val cursor: Cursor) : Iterator<ArchiveMed
|
||||
override fun hasNext(): Boolean = !cursor.isAfterLast
|
||||
|
||||
override fun next(): ArchiveMediaItem {
|
||||
val plaintextHash = cursor.requireNonNullBlob(AttachmentTable.DATA_HASH_END)
|
||||
val remoteKey = cursor.requireNonNullBlob(AttachmentTable.REMOTE_KEY)
|
||||
val plaintextHash = cursor.requireNonNullString(AttachmentTable.DATA_HASH_END).decodeBase64OrThrow()
|
||||
val remoteKey = cursor.requireNonNullString(AttachmentTable.REMOTE_KEY).decodeBase64OrThrow()
|
||||
val cdn = cursor.requireIntOrNull(AttachmentTable.ARCHIVE_CDN)
|
||||
|
||||
val mediaId = MediaName.fromPlaintextHashAndRemoteKey(plaintextHash, remoteKey).toMediaId(SignalStore.backup.mediaRootBackupKey).encode()
|
||||
|
||||
@@ -25,7 +25,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -77,6 +76,8 @@ import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ArchiveAttachmentReconciliationJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
@@ -149,9 +150,9 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
mainContent = {
|
||||
Screen(
|
||||
state = state,
|
||||
onBackupTierSelected = { tier -> viewModel.onBackupTierSelected(tier) },
|
||||
onCheckRemoteBackupStateClicked = { viewModel.checkRemoteBackupState() },
|
||||
onEnqueueRemoteBackupClicked = { viewModel.triggerBackupJob() },
|
||||
onEnqueueReconciliationClicked = { AppDependencies.jobManager.add(ArchiveAttachmentReconciliationJob(forced = true)) },
|
||||
onHaltAllBackupJobsClicked = { viewModel.haltAllJobs() },
|
||||
onValidateBackupClicked = { viewModel.validateBackup() },
|
||||
onSaveEncryptedBackupToDiskClicked = {
|
||||
@@ -222,7 +223,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
onDeleteRemoteBackup = {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will delete all of your remote backup data?")
|
||||
.setMessage("This will delete all of your remote backup data!")
|
||||
.setPositiveButton("Delete remote data") { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
val success = viewModel.deleteRemoteBackupData()
|
||||
@@ -234,6 +235,21 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
},
|
||||
onClearLocalMediaBackupState = {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Are you sure?")
|
||||
.setMessage("This will cause you to have to re-upload all of your media!")
|
||||
.setPositiveButton("Clear local media state") { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
viewModel.clearLocalMediaBackupState()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(requireContext(), "Done!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
},
|
||||
onDisplayInitialBackupFailureSheet = {
|
||||
BackupRepository.displayInitialBackupFailureNotification()
|
||||
BackupAlertBottomSheet
|
||||
@@ -312,8 +328,8 @@ fun Screen(
|
||||
onImportNewStyleLocalBackupClicked: () -> Unit = {},
|
||||
onCheckRemoteBackupStateClicked: () -> Unit = {},
|
||||
onEnqueueRemoteBackupClicked: () -> Unit = {},
|
||||
onEnqueueReconciliationClicked: () -> Unit = {},
|
||||
onWipeDataAndRestoreFromRemoteClicked: () -> Unit = {},
|
||||
onBackupTierSelected: (MessageBackupTier?) -> Unit = {},
|
||||
onHaltAllBackupJobsClicked: () -> Unit = {},
|
||||
onSavePlaintextCopyOfRemoteBackupClicked: () -> Unit = {},
|
||||
onValidateBackupClicked: () -> Unit = {},
|
||||
@@ -322,6 +338,7 @@ fun Screen(
|
||||
onImportEncryptedBackupFromDiskClicked: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskDismissed: () -> Unit = {},
|
||||
onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> },
|
||||
onClearLocalMediaBackupState: () -> Unit = {},
|
||||
onDeleteRemoteBackup: () -> Unit = {},
|
||||
onDisplayInitialBackupFailureSheet: () -> Unit = {}
|
||||
) {
|
||||
@@ -353,21 +370,6 @@ fun Screen(
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Tier", fontWeight = FontWeight.Bold)
|
||||
options.forEach { option ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = option.value == state.backupTier,
|
||||
onClick = { onBackupTierSelected(option.value) }
|
||||
)
|
||||
Text(option.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Rows.TextRow(
|
||||
text = {
|
||||
Text(
|
||||
@@ -392,6 +394,12 @@ fun Screen(
|
||||
onClick = onEnqueueRemoteBackupClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Enqueue reconciliation job",
|
||||
label = "Schedules a job that will ensure local and remote media state are in sync.",
|
||||
onClick = onEnqueueReconciliationClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Halt all backup jobs",
|
||||
label = "Stops all backup-related jobs to the best of our ability.",
|
||||
@@ -513,6 +521,12 @@ fun Screen(
|
||||
onClick = onDeleteRemoteBackup
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Clear local media backup state",
|
||||
label = "Resets local state tracking so you think you haven't uploaded any media. The media still exists on the server.",
|
||||
onClick = onClearLocalMediaBackupState
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
|
||||
Rows.TextRow(
|
||||
|
||||
@@ -292,11 +292,6 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackupTierSelected(backupTier: MessageBackupTier?) {
|
||||
SignalStore.backup.backupTier = backupTier
|
||||
_state.value = _state.value.copy(backupTier = backupTier)
|
||||
}
|
||||
|
||||
fun onImportSelected() {
|
||||
_state.value = _state.value.copy(dialog = DialogState.ImportCredentials)
|
||||
}
|
||||
@@ -398,6 +393,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
suspend fun clearLocalMediaBackupState() = withContext(Dispatchers.IO) {
|
||||
SignalDatabase.attachments.clearAllArchiveData()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ fun InternalBackupStatsTab(stats: InternalBackupPlaygroundViewModel.StatsState,
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Text(text = "Unique/archived data files: ${stats.attachmentStats.attachmentFileCount}/${stats.attachmentStats.finishedAttachmentFileCount}")
|
||||
Text(text = "Unique/archived verified digest count: ${stats.attachmentStats.attachmentPlaintextHashAndKeyCount}/${stats.attachmentStats.finishedAttachmentPlaintextHashAndKeyCount}")
|
||||
Text(text = "Unique/archived verified plaintextHash count: ${stats.attachmentStats.attachmentPlaintextHashAndKeyCount}/${stats.attachmentStats.finishedAttachmentPlaintextHashAndKeyCount}")
|
||||
Text(text = "Unique/expected thumbnail files: ${stats.attachmentStats.thumbnailFileCount}/${stats.attachmentStats.estimatedThumbnailCount}")
|
||||
Text(text = "Local Total: ${stats.attachmentStats.attachmentFileCount + stats.attachmentStats.thumbnailFileCount}")
|
||||
Text(text = "Expected remote total: ${stats.attachmentStats.estimatedThumbnailCount + stats.attachmentStats.finishedAttachmentPlaintextHashAndKeyCount}")
|
||||
|
||||
@@ -679,15 +679,15 @@ class AttachmentTable(
|
||||
/**
|
||||
* Sets the archive transfer state for the given attachment by digest.
|
||||
*/
|
||||
fun resetArchiveTransferStateByPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray) {
|
||||
writableDatabase
|
||||
fun resetArchiveTransferStateByPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray): Boolean {
|
||||
return writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(
|
||||
ARCHIVE_TRANSFER_STATE to ArchiveTransferState.NONE.value,
|
||||
ARCHIVE_CDN to null
|
||||
)
|
||||
.where("$DATA_HASH_END = ? AND $REMOTE_KEY = ?", Base64.encodeWithPadding(plaintextHash), Base64.encodeWithPadding(remoteKey))
|
||||
.run()
|
||||
.run() > 0
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2593,11 +2593,11 @@ class AttachmentTable(
|
||||
.associate { it to readableDatabase.count().from(TABLE_NAME).where("$ARCHIVE_TRANSFER_STATE = ${it.value} AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL").run().readToSingleLong(-1L) }
|
||||
|
||||
val attachmentFileCount = readableDatabase.query("SELECT COUNT(DISTINCT $DATA_FILE) FROM $TABLE_NAME WHERE $DATA_FILE NOT NULL AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL").readToSingleLong(-1L)
|
||||
val finishedAttachmentFileCount = readableDatabase.query("SELECT COUNT(DISTINCT $DATA_FILE) FROM $TABLE_NAME WHERE $DATA_FILE NOT NULL AND $DATA_HASH_END NOT NULL $REMOTE_KEY NOT NULL AND $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value}").readToSingleLong(-1L)
|
||||
val finishedAttachmentFileCount = readableDatabase.query("SELECT COUNT(DISTINCT $DATA_FILE) FROM $TABLE_NAME WHERE $DATA_FILE NOT NULL AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value}").readToSingleLong(-1L)
|
||||
val attachmentPlaintextHashAndKeyCount = readableDatabase.query("SELECT COUNT(*) FROM (SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY FROM $TABLE_NAME WHERE $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND $TRANSFER_STATE in ($TRANSFER_PROGRESS_DONE, $TRANSFER_RESTORE_OFFLOADED, $TRANSFER_RESTORE_IN_PROGRESS, $TRANSFER_NEEDS_RESTORE))").readToSingleLong(-1L)
|
||||
val finishedAttachmentDigestCount = readableDatabase.query("SELECT COUNT(*) FROM (SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY) FROM $TABLE_NAME WHERE $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value})").readToSingleLong(-1L)
|
||||
val finishedAttachmentDigestCount = readableDatabase.query("SELECT COUNT(*) FROM (SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY FROM $TABLE_NAME WHERE $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value})").readToSingleLong(-1L)
|
||||
val thumbnailFileCount = readableDatabase.query("SELECT COUNT(DISTINCT $THUMBNAIL_FILE) FROM $TABLE_NAME WHERE $THUMBNAIL_FILE IS NOT NULL").readToSingleLong(-1L)
|
||||
val estimatedThumbnailCount = readableDatabase.query("SELECT COUNT(*) FROM (SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY) FROM $TABLE_NAME WHERE $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value} AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND ($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%'))").readToSingleLong(-1L)
|
||||
val estimatedThumbnailCount = readableDatabase.query("SELECT COUNT(*) FROM (SELECT DISTINCT $DATA_HASH_END, $REMOTE_KEY FROM $TABLE_NAME WHERE $ARCHIVE_TRANSFER_STATE = ${ArchiveTransferState.FINISHED.value} AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL AND ($CONTENT_TYPE LIKE 'image/%' OR $CONTENT_TYPE LIKE 'video/%'))").readToSingleLong(-1L)
|
||||
|
||||
val pendingUploadBytes = getPendingArchiveUploadBytes()
|
||||
val uploadedAttachmentBytes = readableDatabase
|
||||
|
||||
@@ -127,7 +127,13 @@ class ArchiveAttachmentReconciliationJob private constructor(
|
||||
val entry = BackupMediaSnapshotTable.MediaEntry.fromCursor(it)
|
||||
// TODO [backup] Re-enqueue thumbnail uploads if necessary
|
||||
if (!entry.isThumbnail) {
|
||||
SignalDatabase.attachments.resetArchiveTransferStateByPlaintextHashAndRemoteKey(entry.plaintextHash, entry.remoteKey)
|
||||
val success = SignalDatabase.attachments.resetArchiveTransferStateByPlaintextHashAndRemoteKey(entry.plaintextHash, entry.remoteKey)
|
||||
if (!success) {
|
||||
Log.e(TAG, "Failed to reset archive transfer state by remote hash/key!")
|
||||
if (RemoteConfig.internalUser) {
|
||||
throw RuntimeException("Failed to reset archive transfer state by remote hash/key!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ class ArchiveThumbnailUploadJob private constructor(
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (attachment.remoteDigest == null) {
|
||||
Log.w(TAG, "$attachmentId was never uploaded! Cannot proceed.")
|
||||
if (attachment.remoteDigest == null && attachment.dataHash == null) {
|
||||
Log.w(TAG, "$attachmentId has no integrity check! Cannot proceed.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class ArchiveThumbnailUploadJob private constructor(
|
||||
data = thumbnailResult.data
|
||||
)
|
||||
|
||||
Log.d(TAG, "Successfully archived thumbnail for $attachmentId mediaName=${attachment.requireThumbnailMediaName()}")
|
||||
Log.d(TAG, "Successfully archived thumbnail for $attachmentId")
|
||||
Result.success()
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
|
||||
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
@@ -277,12 +278,18 @@ class AttachmentDownloadJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (attachment.remoteDigest == null && attachment.dataHash == null) {
|
||||
Log.w(TAG, "Attachment has no integrity check!")
|
||||
throw InvalidAttachmentException("Attachment has no integrity check!")
|
||||
}
|
||||
|
||||
val decryptingStream = AppDependencies
|
||||
.signalServiceMessageReceiver
|
||||
.retrieveAttachment(
|
||||
pointer,
|
||||
attachmentFile,
|
||||
maxReceiveSize,
|
||||
IntegrityCheck.forEncryptedDigestAndPlaintextHash(attachment.remoteDigest, attachment.dataHash),
|
||||
progressListener
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException;
|
||||
@@ -84,9 +86,16 @@ public final class AvatarGroupsV1DownloadJob extends BaseJob {
|
||||
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
|
||||
attachment.deleteOnExit();
|
||||
|
||||
SignalServiceMessageReceiver receiver = AppDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId.V2(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, Optional.empty(), 0, fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis(), null);
|
||||
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE);
|
||||
|
||||
SignalServiceMessageReceiver receiver = AppDependencies.getSignalServiceMessageReceiver();
|
||||
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(0, new SignalServiceAttachmentRemoteId.V2(avatarId), contentType, key, Optional.of(0), Optional.empty(), 0, 0, digest, Optional.empty(), 0, fileName, false, false, false, Optional.empty(), Optional.empty(), System.currentTimeMillis(), null);
|
||||
|
||||
if (pointer.getDigest().isEmpty()) {
|
||||
throw new InvalidMessageException("Missing digest!");
|
||||
}
|
||||
|
||||
IntegrityCheck integrityCheck = IntegrityCheck.forEncryptedDigest(pointer.getDigest().get());
|
||||
InputStream inputStream = receiver.retrieveAttachment(pointer, attachment, AvatarHelper.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE, integrityCheck);
|
||||
|
||||
AvatarHelper.setAvatar(context, record.get().getRecipientId(), inputStream);
|
||||
SignalDatabase.groups().onAvatarUpdated(groupId, true);
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream
|
||||
@@ -59,7 +60,7 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo
|
||||
try {
|
||||
val contactsFile: File = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(context)
|
||||
AppDependencies.signalServiceMessageReceiver
|
||||
.retrieveAttachment(contactAttachment, contactsFile, MAX_ATTACHMENT_SIZE)
|
||||
.retrieveAttachment(contactAttachment, contactsFile, MAX_ATTACHMENT_SIZE, IntegrityCheck.forEncryptedDigest(contactAttachment.digest.get()))
|
||||
.use(this::processContactFile)
|
||||
} catch (e: MissingConfigurationException) {
|
||||
throw IOException(e)
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
|
||||
import org.whispersystems.signalservice.api.messages.AttachmentTransferProgress
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException
|
||||
@@ -49,6 +50,7 @@ import org.whispersystems.signalservice.api.push.exceptions.RangeException
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.math.max
|
||||
import kotlin.math.pow
|
||||
import kotlin.time.Duration.Companion.days
|
||||
@@ -172,12 +174,12 @@ class RestoreAttachmentJob private constructor(
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment no longer exists.")
|
||||
Log.w(TAG, "[$attachmentId] Attachment no longer exists.")
|
||||
return
|
||||
}
|
||||
|
||||
if (attachment.isPermanentlyFailed) {
|
||||
Log.w(TAG, "Attachment was marked as a permanent failure. Refusing to download.")
|
||||
Log.w(TAG, "[$attachmentId] Attachment was marked as a permanent failure. Refusing to download.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -186,7 +188,7 @@ class RestoreAttachmentJob private constructor(
|
||||
attachment.transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED &&
|
||||
attachment.transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED
|
||||
) {
|
||||
Log.w(TAG, "Attachment does not need to be restored. Current state: ${attachment.transferState}")
|
||||
Log.w(TAG, "[$attachmentId] Attachment does not need to be restored. Current state: ${attachment.transferState}")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -231,14 +233,20 @@ class RestoreAttachmentJob private constructor(
|
||||
var archiveFile: File? = null
|
||||
var useArchiveCdn = false
|
||||
|
||||
if (attachment.remoteDigest == null && attachment.dataHash == null) {
|
||||
Log.w(TAG, "[$attachmentId] Attachment has no integrity check! Cannot proceed.")
|
||||
markPermanentlyFailed(attachmentId)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (attachment.size > maxReceiveSize) {
|
||||
throw MmsException("Attachment too large, failing download")
|
||||
throw MmsException("[$attachmentId] Attachment too large, failing download")
|
||||
}
|
||||
|
||||
useArchiveCdn = if (SignalStore.backup.backsUpMedia && !forceTransitTier) {
|
||||
if (attachment.archiveTransferState != AttachmentTable.ArchiveTransferState.FINISHED) {
|
||||
throw InvalidAttachmentException("Invalid attachment configuration! backsUpMedia: ${SignalStore.backup.backsUpMedia}, forceTransitTier: $forceTransitTier, archiveTransferState: ${attachment.archiveTransferState}")
|
||||
throw InvalidAttachmentException("[$attachmentId] Invalid attachment configuration! backsUpMedia: ${SignalStore.backup.backsUpMedia}, forceTransitTier: $forceTransitTier, archiveTransferState: ${attachment.archiveTransferState}")
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -259,7 +267,9 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
|
||||
val decryptingStream = if (useArchiveCdn) {
|
||||
archiveFile = SignalDatabase.attachments.getOrCreateArchiveTransferFile(attachmentId)
|
||||
// TODO next PR: remove archive transfer file and just use the regular attachment file
|
||||
archiveFile = attachmentFile
|
||||
// archiveFile = SignalDatabase.attachments.getOrCreateArchiveTransferFile(attachmentId)
|
||||
val cdnCredentials = BackupRepository.getCdnReadCredentials(BackupRepository.CredentialType.MEDIA, attachment.archiveCdn ?: RemoteConfig.backupFallbackArchiveCdn).successOrThrow().headers
|
||||
|
||||
messageReceiver
|
||||
@@ -269,7 +279,6 @@ class RestoreAttachmentJob private constructor(
|
||||
cdnCredentials,
|
||||
archiveFile,
|
||||
pointer,
|
||||
attachmentFile,
|
||||
maxReceiveSize,
|
||||
progressListener
|
||||
)
|
||||
@@ -279,6 +288,7 @@ class RestoreAttachmentJob private constructor(
|
||||
pointer,
|
||||
attachmentFile,
|
||||
maxReceiveSize,
|
||||
IntegrityCheck.forEncryptedDigestAndPlaintextHash(pointer.digest.getOrNull(), attachment.dataHash),
|
||||
progressListener
|
||||
)
|
||||
}
|
||||
@@ -286,7 +296,7 @@ class RestoreAttachmentJob private constructor(
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, decryptingStream, if (manual) System.currentTimeMillis().milliseconds else null)
|
||||
} catch (e: RangeException) {
|
||||
val transferFile = archiveFile ?: attachmentFile
|
||||
Log.w(TAG, "Range exception, file size " + transferFile.length(), e)
|
||||
Log.w(TAG, "[$attachmentId] Range exception, file size " + transferFile.length(), e)
|
||||
if (transferFile.delete()) {
|
||||
Log.i(TAG, "Deleted temp download file to recover")
|
||||
throw RetryLaterException(e)
|
||||
@@ -299,7 +309,7 @@ class RestoreAttachmentJob private constructor(
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
if (SignalStore.backup.backsUpMedia) {
|
||||
if (e.code == 404 && !forceTransitTier && attachment.remoteLocation?.isNotBlank() == true) {
|
||||
Log.i(TAG, "Failed to download attachment from archive! Should only happen for recent attachments in a narrow window. Retrying download from transit CDN.")
|
||||
Log.i(TAG, "[$attachmentId] Failed to download attachment from archive! Should only happen for recent attachments in a narrow window. Retrying download from transit CDN.")
|
||||
if (RemoteConfig.internalUser) {
|
||||
postFailedToDownloadFromArchiveNotification()
|
||||
}
|
||||
@@ -316,18 +326,18 @@ class RestoreAttachmentJob private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
Log.w(TAG, "[$attachmentId] Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
Log.w(TAG, "[$attachmentId] Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: MissingConfigurationException) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e)
|
||||
Log.w(TAG, "[$attachmentId] Experienced exception while trying to download an attachment.", e)
|
||||
markFailed(attachmentId)
|
||||
} catch (e: InvalidMessageException) {
|
||||
Log.w(TAG, "Experienced an InvalidMessageException while trying to download an attachment.", e)
|
||||
Log.w(TAG, "[$attachmentId] Experienced an InvalidMessageException while trying to download an attachment.", e)
|
||||
if (e.cause is InvalidMacException) {
|
||||
Log.w(TAG, "Detected an invalid mac. Treating as a permanent failure.")
|
||||
Log.w(TAG, "[$attachmentId] Detected an invalid mac. Treating as a permanent failure.")
|
||||
markPermanentlyFailed(attachmentId)
|
||||
} else {
|
||||
markFailed(attachmentId)
|
||||
|
||||
@@ -120,7 +120,6 @@ class RestoreAttachmentThumbnailJob private constructor(
|
||||
|
||||
val maxThumbnailSize: Long = RemoteConfig.maxAttachmentReceiveSizeBytes
|
||||
val thumbnailTransferFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile()
|
||||
val thumbnailFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile()
|
||||
|
||||
val progressListener = object : SignalServiceAttachment.ProgressListener {
|
||||
override fun onAttachmentProgress(progress: AttachmentTransferProgress) = Unit
|
||||
@@ -137,7 +136,6 @@ class RestoreAttachmentThumbnailJob private constructor(
|
||||
cdnCredentials,
|
||||
thumbnailTransferFile,
|
||||
pointer,
|
||||
thumbnailFile,
|
||||
maxThumbnailSize,
|
||||
progressListener
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.StreamSupplier
|
||||
import java.io.IOException
|
||||
|
||||
@@ -154,7 +155,10 @@ class RestoreLocalAttachmentJob private constructor(
|
||||
streamLength = size,
|
||||
plaintextLength = attachment.size,
|
||||
combinedKeyMaterial = combinedKey,
|
||||
digest = attachment.remoteDigest,
|
||||
integrityCheck = IntegrityCheck.forEncryptedDigestAndPlaintextHash(
|
||||
encryptedDigest = attachment.remoteDigest,
|
||||
plaintextHash = attachment.dataHash
|
||||
),
|
||||
incrementalDigest = null,
|
||||
incrementalMacChunkSize = 0
|
||||
).use { input ->
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.signal.core.util.Base64;
|
||||
import org.whispersystems.signalservice.api.backup.MediaName;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck;
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
|
||||
import org.signal.core.util.stream.TailerInputStream;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
@@ -100,11 +101,13 @@ class PartDataSource implements DataSource {
|
||||
long streamLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size));
|
||||
AttachmentCipherInputStream.StreamSupplier streamSupplier = () -> new TailerInputStream(() -> new FileInputStream(transferFile), streamLength);
|
||||
|
||||
if (attachment.remoteDigest == null) {
|
||||
if (attachment.remoteDigest == null && attachment.dataHash == null) {
|
||||
throw new InvalidMessageException("Missing digest!");
|
||||
}
|
||||
|
||||
this.inputStream = AttachmentCipherInputStream.createForAttachment(streamSupplier, streamLength, attachment.size, decodedKey, attachment.remoteDigest, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize);
|
||||
IntegrityCheck integrityCheck = IntegrityCheck.forEncryptedDigestAndPlaintextHash(attachment.remoteDigest, attachment.dataHash);
|
||||
|
||||
this.inputStream = AttachmentCipherInputStream.createForAttachment(streamSupplier, streamLength, attachment.size, decodedKey, integrityCheck, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new IOException("Error decrypting attachment stream!", e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user