From 26b9cea88ef456d765c9b952f7f0fa76b57d6e4a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 31 Oct 2024 11:58:40 -0400 Subject: [PATCH] Move to separate message and media backup keys. --- .../securesms/backup/v2/BackupRepository.kt | 223 ++++++++++-------- .../DatabaseAttachmentArchiveExtensions.kt | 10 +- .../backup/v2/stream/EncryptedBackupReader.kt | 6 +- .../backup/v2/stream/EncryptedBackupWriter.kt | 6 +- .../MessageBackupsFlowFragment.kt | 2 +- .../subscription/MessageBackupsFlowState.kt | 4 +- .../MessageBackupsKeyRecordScreen.kt | 10 +- .../v2/util/ArchiveConverterExtensions.kt | 4 +- .../remote/BackupKeyDisplayFragment.kt | 2 +- .../InternalBackupPlaygroundFragment.kt | 19 ++ .../InternalBackupPlaygroundViewModel.kt | 9 +- .../jobs/ArchiveThumbnailUploadJob.kt | 9 +- .../securesms/jobs/RestoreAttachmentJob.kt | 2 +- .../jobs/RestoreAttachmentThumbnailJob.kt | 2 +- .../securesms/keyvalue/BackupValues.kt | 115 ++++++--- .../linkdevice/LinkDeviceRepository.kt | 12 +- .../linkdevice/LinkDeviceViewModel.kt | 10 +- .../securesms/video/exo/PartDataSource.java | 7 +- .../stream/EncryptedBackupReaderWriterTest.kt | 8 +- dependencies.gradle.kts | 2 +- gradle/verification-metadata.xml | 28 +-- .../api/SignalServiceMessageReceiver.java | 7 +- .../signalservice/api/archive/ArchiveApi.kt | 117 ++++++--- .../api/archive/ArchiveMediaRequest.kt | 3 +- .../archive/ArchiveServiceCredentialPair.kt | 14 ++ .../ArchiveServiceCredentialsResponse.kt | 26 +- .../api/archive/ArchiveSetBackupIdRequest.kt | 5 +- .../signalservice/api/backup/BackupKey.kt | 92 +------- .../api/backup/MediaRootBackupKey.kt | 59 +++++ .../api/backup/MessageBackupKey.kt | 58 +++++ .../crypto/AttachmentCipherInputStream.java | 12 +- .../signalservice/api/kbs/MasterKey.java | 7 +- .../signalservice/api/link/LinkDeviceApi.kt | 6 +- .../internal/push/PushServiceSocket.java | 4 +- .../api/crypto/AttachmentCipherTest.java | 48 ++-- .../api/crypto/AttachmentCipherTestHelper.kt | 28 +++ 36 files changed, 596 insertions(+), 380 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialPair.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MessageBackupKey.kt create mode 100644 libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTestHelper.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index cc69e7eca0..759d8287a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.backup.v2 import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okio.ByteString import okio.ByteString.Companion.toByteString import org.greenrobot.eventbus.EventBus import org.signal.core.util.Base64 @@ -27,7 +26,6 @@ import org.signal.core.util.stream.NonClosingOutputStream import org.signal.core.util.withinTransaction import org.signal.libsignal.messagebackup.MessageBackup import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult -import org.signal.libsignal.messagebackup.MessageBackupKey import org.signal.libsignal.protocol.ServiceId.Aci import org.signal.libsignal.zkgroup.backups.BackupLevel import org.signal.libsignal.zkgroup.profiles.ProfileKey @@ -73,11 +71,12 @@ import org.whispersystems.signalservice.api.StatusCodeErrorAction import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse -import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential +import org.whispersystems.signalservice.api.archive.ArchiveServiceCredentialPair import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse -import org.whispersystems.signalservice.api.backup.BackupKey import org.whispersystems.signalservice.api.backup.MediaName +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener import org.whispersystems.signalservice.api.push.ServiceId.ACI @@ -96,6 +95,7 @@ import java.util.Locale import java.util.concurrent.atomic.AtomicLong import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds +import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey object BackupRepository { @@ -111,7 +111,8 @@ object BackupRepository { 401 -> { Log.w(TAG, "Received status 401. Resetting initialized state + auth credentials.", error.exception) SignalStore.backup.backupsInitialized = false - SignalStore.backup.clearAllCredentials() + SignalStore.backup.messageCredentials.clearAll() + SignalStore.backup.mediaCredentials.clearAll() } 403 -> { @@ -278,7 +279,7 @@ object BackupRepository { archiveAttachment: (AttachmentTable.LocalArchivableAttachment, () -> InputStream?) -> Unit ) { val writer = EncryptedBackupWriter( - key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), + key = SignalStore.backup.messageBackupKey, aci = SignalStore.account.aci!!, outputStream = NonClosingOutputStream(main), append = { main.write(it) } @@ -310,7 +311,7 @@ object BackupRepository { fun export( outputStream: OutputStream, append: (ByteArray) -> Unit, - backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), + messageBackupKey: org.whispersystems.signalservice.api.backup.MessageBackupKey = SignalStore.backup.messageBackupKey, plaintext: Boolean = false, currentTime: Long = System.currentTimeMillis(), mediaBackupEnabled: Boolean = SignalStore.backup.backsUpMedia, @@ -320,7 +321,7 @@ object BackupRepository { PlainTextBackupWriter(outputStream) } else { EncryptedBackupWriter( - key = backupKey, + key = messageBackupKey, aci = SignalStore.account.aci!!, outputStream = outputStream, append = append @@ -368,7 +369,7 @@ object BackupRepository { BackupInfo( version = VERSION, backupTimeMs = exportState.backupTime, - mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey?.toByteString() ?: ByteString.EMPTY + mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey.value.toByteString() ) ) frameCount++ @@ -444,7 +445,7 @@ object BackupRepository { } fun localImport(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData): ImportResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey val frameReader = try { EncryptedBackupReader( @@ -464,7 +465,7 @@ object BackupRepository { } fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false, cancellationSignal: () -> Boolean = { false }): ImportResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey val frameReader = if (plaintext) { PlainTextBackupReader(inputStreamFactory(), length) @@ -483,7 +484,7 @@ object BackupRepository { } private fun import( - backupKey: BackupKey, + messageBackupKey: MessageBackupKey, frameReader: BackupImportReader, selfData: SelfData, cancellationSignal: () -> Boolean @@ -557,7 +558,8 @@ object BackupRepository { return ImportResult.Failure } - SignalStore.backup.mediaRootBackupKey = header.mediaRootBackupKey.toByteArray() + val mediaRootBackupKey = MediaRootBackupKey(header.mediaRootBackupKey.toByteArray()) + SignalStore.backup.mediaRootBackupKey = mediaRootBackupKey // Add back self after clearing data val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true) @@ -567,7 +569,7 @@ object BackupRepository { // Add back default All Chats chat folder after clearing data SignalDatabase.chatFolders.insertAllChatFolder() - val importState = ImportState(backupKey) + val importState = ImportState(messageBackupKey, mediaRootBackupKey) val chatItemInserter: ChatItemArchiveImporter = ChatItemArchiveProcessor.beginImport(importState) Log.d(TAG, "[import] Beginning to read frames.") @@ -695,32 +697,35 @@ object BackupRepository { fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult { val masterKey = SignalStore.svr.getOrCreateMasterKey() - val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray())) + val key = LibSignalMessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray())) return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length) } fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getArchiveMediaItemsPage(backupKey, SignalStore.account.requireAci(), credential, limit, cursor) + SignalNetwork.archive.getArchiveMediaItemsPage(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential, limit, cursor) } } fun getRemoteBackupUsedSpace(): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential) .map { it.usedSpace } } } /** - * If backups are enabled, sync with the network. Otherwise, return a 404. + * If backups are enabled, sync with the network. Otherwise, return a 404.a + * Used in instrumentation tests. */ fun getBackupTier(): NetworkResult { return if (SignalStore.backup.areBackupsEnabled) { @@ -736,11 +741,12 @@ object BackupRepository { * to be the case. */ private fun getBackupTier(aci: ACI): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .map { credential -> - val zkCredential = SignalNetwork.archive.getZkCredential(backupKey, aci, credential) + val zkCredential = SignalNetwork.archive.getZkCredential(backupKey, aci, credential.mediaCredential) if (zkCredential.backupLevel == BackupLevel.PAID) { MessageBackupTier.PAID } else { @@ -753,16 +759,17 @@ object BackupRepository { * Returns an object with details about the remote backup state. */ fun getRemoteBackupState(): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential) .map { it to credential } } .then { pair -> val (info, credential) = pair - SignalNetwork.archive.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.debugGetUploadedMediaItemMetadata(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential) .also { Log.i(TAG, "MediaItemMetadataResult: $it") } .map { mediaObjects -> BackupMetadata( @@ -779,11 +786,12 @@ object BackupRepository { * @return True if successful, otherwise false. */ fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getMessageBackupUploadForm(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getMessageBackupUploadForm(backupKey, SignalStore.account.requireAci(), credential.messageCredential) .also { Log.i(TAG, "UploadFormResult: $it") } } .then { form -> @@ -799,11 +807,12 @@ object BackupRepository { } fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential) } .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .map { pair -> @@ -814,11 +823,12 @@ object BackupRepository { } fun getBackupFileLastModified(): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential) } .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .then { pair -> @@ -833,12 +843,13 @@ object BackupRepository { /** * Returns an object with details about the remote backup state. */ - fun debugGetArchivedMediaState(): NetworkResult> { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + private fun debugGetArchivedMediaState(): NetworkResult> { + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.debugGetUploadedMediaItemMetadata(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential) } } @@ -849,11 +860,12 @@ object BackupRepository { * It's important to note that in order to get this to the archive cdn, you still need to use [copyAttachmentToArchive]. */ fun getAttachmentUploadForm(): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getMediaUploadForm(backupKey, SignalStore.account.requireAci(), credential) + SignalNetwork.archive.getMediaUploadForm(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential) } } @@ -861,15 +873,16 @@ object BackupRepository { * Copies a thumbnail that has been uploaded to the transit cdn to the archive cdn. */ fun copyThumbnailToArchive(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() - val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey) + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey + val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), mediaRootBackupKey) - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> SignalNetwork.archive.copyAttachmentToArchive( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, item = request ) } @@ -879,32 +892,34 @@ object BackupRepository { * Copies an attachment that has been uploaded to the transit cdn to the archive cdn. */ fun copyAttachmentToArchive(attachment: DatabaseAttachment): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> val mediaName = attachment.getMediaName() - val request = attachment.toArchiveMediaRequest(mediaName, backupKey) + val request = attachment.toArchiveMediaRequest(mediaName, mediaRootBackupKey) SignalNetwork.archive .copyAttachmentToArchive( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, item = request ) .map { Triple(mediaName, request.mediaId, it) } } .map { (mediaName, mediaId, response) -> - val thumbnailId = backupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode() + val thumbnailId = mediaRootBackupKey.deriveMediaId(attachment.getThumbnailMediaName()).encode() SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId, archiveThumbnailMediaId = thumbnailId) } .also { Log.i(TAG, "archiveMediaResult: $it") } } fun copyAttachmentToArchive(databaseAttachments: List): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> val requests = mutableListOf() val mediaIdToAttachmentId = mutableMapOf() @@ -912,7 +927,7 @@ object BackupRepository { databaseAttachments.forEach { val mediaName = it.getMediaName() - val request = it.toArchiveMediaRequest(mediaName, backupKey) + val request = it.toArchiveMediaRequest(mediaName, mediaRootBackupKey) requests += request mediaIdToAttachmentId[request.mediaId] = it.attachmentId attachmentIdToMediaName[it.attachmentId] = mediaName.name @@ -920,9 +935,9 @@ object BackupRepository { SignalNetwork.archive .copyAttachmentToArchive( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, items = requests ) .map { BatchArchiveMediaResult(it, mediaIdToAttachmentId, attachmentIdToMediaName) } @@ -933,7 +948,7 @@ object BackupRepository { .forEach { val attachmentId = result.mediaIdToAttachmentId(it.mediaId) val mediaName = result.attachmentIdToMediaName(attachmentId) - val thumbnailId = backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode() + val thumbnailId = mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(mediaName = mediaName)).encode() SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId, thumbnailId) } result @@ -942,7 +957,8 @@ object BackupRepository { } fun deleteArchivedMedia(attachments: List): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val mediaToDelete = attachments .filter { it.archiveMediaId != null } @@ -958,12 +974,12 @@ object BackupRepository { return NetworkResult.Success(Unit) } - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> SignalNetwork.archive.deleteArchivedMedia( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, mediaToDelete = mediaToDelete ) } @@ -974,7 +990,8 @@ object BackupRepository { } fun deleteAbandonedMediaObjects(mediaObjects: Collection): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val mediaToDelete = mediaObjects .map { @@ -989,12 +1006,12 @@ object BackupRepository { return NetworkResult.Success(Unit) } - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> SignalNetwork.archive.deleteArchivedMedia( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, mediaToDelete = mediaToDelete ) } @@ -1002,7 +1019,7 @@ object BackupRepository { } fun debugDeleteAllArchivedMedia(): NetworkResult { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey return debugGetArchivedMediaState() .then { archivedMedia -> @@ -1018,12 +1035,12 @@ object BackupRepository { Log.i(TAG, "No media to delete, quick success") NetworkResult.Success(Unit) } else { - getAuthCredential() + getAuthCredentialPair() .then { credential -> SignalNetwork.archive.deleteArchivedMedia( - backupKey = backupKey, + mediaRootBackupKey = mediaRootBackupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential, + serviceCredential = credential.mediaCredential, mediaToDelete = mediaToDelete ) } @@ -1044,15 +1061,16 @@ object BackupRepository { return NetworkResult.Success(cached) } - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> SignalNetwork.archive.getCdnReadCredentials( cdnNumber = cdnNumber, - backupKey = backupKey, + messageBackupKey = backupKey, aci = SignalStore.account.requireAci(), - serviceCredential = credential + serviceCredential = credential.mediaCredential ) } .also { @@ -1107,11 +1125,12 @@ object BackupRepository { ) } - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey - return initBackupAndFetchAuth(backupKey) + return initBackupAndFetchAuth(backupKey, mediaRootBackupKey) .then { credential -> - SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential).map { + SignalNetwork.archive.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential.messageCredential).map { SignalStore.backup.usedBackupMediaSpace = it.usedSpace ?: 0L BackupDirectories(it.backupDir!!, it.mediaDir!!) } @@ -1179,14 +1198,15 @@ object BackupRepository { * Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential. * Should be the basis of all backup operations. */ - private fun initBackupAndFetchAuth(backupKey: BackupKey): NetworkResult { + private fun initBackupAndFetchAuth(messageBackupKey: MessageBackupKey, mediaRootBackupKey: MediaRootBackupKey): NetworkResult { return if (SignalStore.backup.backupsInitialized) { - getAuthCredential().runOnStatusCodeError(resetInitializedStateErrorAction) + getAuthCredentialPair().runOnStatusCodeError(resetInitializedStateErrorAction) } else { return SignalNetwork.archive - .triggerBackupIdReservation(backupKey, SignalStore.account.requireAci()) - .then { getAuthCredential() } - .then { credential -> SignalNetwork.archive.setPublicKey(backupKey, SignalStore.account.requireAci(), credential).map { credential } } + .triggerBackupIdReservation(messageBackupKey, mediaRootBackupKey, SignalStore.account.requireAci()) + .then { getAuthCredentialPair() } + .then { credential -> SignalNetwork.archive.setPublicKey(messageBackupKey, SignalStore.account.requireAci(), credential.messageCredential).map { credential } } + .then { credential -> SignalNetwork.archive.setPublicKey(mediaRootBackupKey, SignalStore.account.requireAci(), credential.mediaCredential).map { credential } } .runIfSuccessful { SignalStore.backup.backupsInitialized = true } .runOnStatusCodeError(resetInitializedStateErrorAction) } @@ -1195,21 +1215,29 @@ object BackupRepository { /** * Retrieves an auth credential, preferring a cached value if available. */ - private fun getAuthCredential(): NetworkResult { + private fun getAuthCredentialPair(): NetworkResult { val currentTime = System.currentTimeMillis() - val credential = SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds) + val messageCredential = SignalStore.backup.messageCredentials.byDay.getForCurrentTime(currentTime.milliseconds) + val mediaCredential = SignalStore.backup.mediaCredentials.byDay.getForCurrentTime(currentTime.milliseconds) - if (credential != null) { - return NetworkResult.Success(credential) + if (messageCredential != null && mediaCredential != null) { + return NetworkResult.Success(ArchiveServiceCredentialPair(messageCredential, mediaCredential)) } Log.w(TAG, "No credentials found for today, need to fetch new ones! This shouldn't happen under normal circumstances. We should ensure the routine fetch is running properly.") return SignalNetwork.archive.getServiceCredentials(currentTime).map { result -> - SignalStore.backup.addCredentials(result.credentials.toList()) - SignalStore.backup.clearCredentialsOlderThan(currentTime) - SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)!! + SignalStore.backup.messageCredentials.add(result.messageCredentials) + SignalStore.backup.messageCredentials.clearOlderThan(currentTime) + + SignalStore.backup.mediaCredentials.add(result.mediaCredentials) + SignalStore.backup.mediaCredentials.clearOlderThan(currentTime) + + ArchiveServiceCredentialPair( + messageCredential = SignalStore.backup.messageCredentials.byDay.getForCurrentTime(currentTime.milliseconds)!!, + mediaCredential = SignalStore.backup.mediaCredentials.byDay.getForCurrentTime(currentTime.milliseconds)!! + ) } } @@ -1232,8 +1260,8 @@ object BackupRepository { return MediaName.fromDigestForThumbnail(remoteDigest!!) } - private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest { - val mediaSecrets = backupKey.deriveMediaSecrets(mediaName) + private fun Attachment.toArchiveMediaRequest(mediaName: MediaName, mediaRootBackupKey: MediaRootBackupKey): ArchiveMediaRequest { + val mediaSecrets = mediaRootBackupKey.deriveMediaSecrets(mediaName) return ArchiveMediaRequest( sourceAttachment = ArchiveMediaRequest.SourceAttachment( @@ -1243,8 +1271,7 @@ object BackupRepository { objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(), mediaId = mediaSecrets.id.encode(), hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey), - encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey), - iv = Base64.encodeWithPadding(mediaSecrets.iv) + encryptionKey = Base64.encodeWithPadding(mediaSecrets.aesKey) ) } @@ -1269,7 +1296,7 @@ class ExportState(val backupTime: Long, val mediaBackupEnabled: Boolean) { val localToRemoteCustomChatColors: MutableMap = hashMapOf() } -class ImportState(val backupKey: BackupKey) { +class ImportState(val messageBackupKey: MessageBackupKey, val mediaRootBackupKey: MediaRootBackupKey) { val remoteToLocalRecipientId: MutableMap = hashMapOf() val chatIdToLocalThreadId: MutableMap = hashMapOf() val chatIdToLocalRecipientId: MutableMap = hashMapOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/DatabaseAttachmentArchiveExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/DatabaseAttachmentArchiveExtensions.kt index 7b528e0544..f11d27e578 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/DatabaseAttachmentArchiveExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/DatabaseAttachmentArchiveExtensions.kt @@ -34,13 +34,13 @@ fun DatabaseAttachment.createArchiveAttachmentPointer(useArchiveCdn: Boolean): S return try { val (remoteId, cdnNumber) = if (useArchiveCdn) { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow() val id = SignalServiceAttachmentRemoteId.Backup( backupDir = backupDirectories.backupDir, mediaDir = backupDirectories.mediaDir, - mediaId = backupKey.deriveMediaId(MediaName(archiveMediaName!!)).encode() + mediaId = mediaRootBackupKey.deriveMediaId(MediaName(archiveMediaName!!)).encode() ) id to archiveCdn @@ -91,11 +91,11 @@ fun DatabaseAttachment.createArchiveThumbnailPointer(): SignalServiceAttachmentP throw InvalidAttachmentException("empty encrypted key") } - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow() return try { - val key = backupKey.deriveThumbnailTransitKey(getThumbnailMediaName()) - val mediaId = backupKey.deriveMediaId(getThumbnailMediaName()).encode() + val key = mediaRootBackupKey.deriveThumbnailTransitKey(getThumbnailMediaName()) + val mediaId = mediaRootBackupKey.deriveMediaId(getThumbnailMediaName()).encode() SignalServiceAttachmentPointer( cdnNumber = archiveCdn, remoteId = SignalServiceAttachmentRemoteId.Backup( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt index cf9c341eee..b0b298e79e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt @@ -13,7 +13,7 @@ import org.signal.core.util.stream.LimitedInputStream import org.signal.core.util.stream.MacInputStream import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.Frame -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import java.io.EOFException import java.io.IOException @@ -31,7 +31,7 @@ import javax.crypto.spec.SecretKeySpec * that decrypted data is gunzipped, then that data is read as frames. */ class EncryptedBackupReader( - key: BackupKey, + key: MessageBackupKey, aci: ACI, val length: Long, dataStream: () -> InputStream @@ -51,7 +51,7 @@ class EncryptedBackupReader( val iv = countingStream.readNBytesOrThrow(16) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv)) + init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv)) } stream = GZIPInputStream( diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt index fcbb64d904..d8cc3600f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt @@ -10,7 +10,7 @@ import org.signal.core.util.writeVarInt32 import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import java.io.IOException import java.io.OutputStream @@ -27,7 +27,7 @@ import javax.crypto.spec.SecretKeySpec * to the end of the [outputStream]. */ class EncryptedBackupWriter( - key: BackupKey, + key: MessageBackupKey, aci: ACI, private val outputStream: OutputStream, private val append: (ByteArray) -> Unit @@ -44,7 +44,7 @@ class EncryptedBackupWriter( outputStream.flush() val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv)) + init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv)) } val mac = Mac.getInstance("HmacSHA256").apply { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index e19da17df1..a78f445c59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -103,7 +103,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega val context = LocalContext.current MessageBackupsKeyRecordScreen( - backupKey = state.backupKey, + messageBackupKey = state.messageBackupKey, onNavigationClick = viewModel::goToPreviousStage, onNextClick = viewModel::goToNextStage, onCopyToClipboardClick = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt index 844c761a41..38ddcfb053 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt @@ -8,7 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey data class MessageBackupsFlowState( val hasBackupSubscriberAvailable: Boolean = false, @@ -18,6 +18,6 @@ data class MessageBackupsFlowState( val inAppPayment: InAppPaymentTable.InAppPayment? = null, val startScreen: MessageBackupsStage, val stage: MessageBackupsStage = startScreen, - val backupKey: BackupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), + val messageBackupKey: MessageBackupKey = SignalStore.backup.messageBackupKey, val failure: Throwable? = null ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt index 73e2101f9e..23ff8b197a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt @@ -50,7 +50,7 @@ import org.signal.core.ui.SignalPreview import org.signal.core.ui.theme.SignalTheme import org.signal.core.util.Hex import org.thoughtcrime.securesms.R -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import kotlin.random.Random /** @@ -60,7 +60,7 @@ import kotlin.random.Random @OptIn(ExperimentalMaterial3Api::class) @Composable fun MessageBackupsKeyRecordScreen( - backupKey: BackupKey, + messageBackupKey: MessageBackupKey, onNavigationClick: () -> Unit = {}, onCopyToClipboardClick: (String) -> Unit = {}, onNextClick: () -> Unit = {} @@ -104,8 +104,8 @@ fun MessageBackupsKeyRecordScreen( modifier = Modifier.padding(top = 12.dp) ) - val backupKeyString = remember(backupKey) { - backupKey.value.toList().chunked(2).map { Hex.toStringCondensed(it.toByteArray()) }.joinToString(" ") + val backupKeyString = remember(messageBackupKey) { + messageBackupKey.value.toList().chunked(2).map { Hex.toStringCondensed(it.toByteArray()) }.joinToString(" ") } Box( @@ -258,7 +258,7 @@ private fun BottomSheetContent( private fun MessageBackupsKeyRecordScreenPreview() { Previews.Preview { MessageBackupsKeyRecordScreen( - backupKey = BackupKey(Random.nextBytes(32)) + messageBackupKey = MessageBackupKey(Random.nextBytes(32)) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt index 352f0b7d7e..2f5b4c2832 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt @@ -96,8 +96,8 @@ fun FilePointer?.toLocalAttachment( cdnKey = this.backupLocator.transitCdnKey, archiveCdn = this.backupLocator.cdnNumber, archiveMediaName = this.backupLocator.mediaName, - archiveMediaId = importState.backupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(), - archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(), + archiveMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(), + archiveThumbnailMediaId = importState.mediaRootBackupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(), digest = this.backupLocator.digest.toByteArray(), incrementalMac = this.incrementalMac?.toByteArray(), incrementalMacChunkSize = this.incrementalMacChunkSize, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt index 40dce01740..5497ee7f56 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt @@ -19,7 +19,7 @@ class BackupKeyDisplayFragment : ComposeFragment() { @Composable override fun FragmentContent() { MessageBackupsKeyRecordScreen( - backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), + messageBackupKey = SignalStore.backup.messageBackupKey, onNavigationClick = { findNavController().popBackStack() }, onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it) }, onNextClick = { findNavController().popBackStack() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index 7ed8302521..342e352a65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -366,6 +366,25 @@ fun Screen( Dividers.Default() + Buttons.LargeTonal( + onClick = { + SignalStore.backup.backupsInitialized = false + } + ) { + Text("Clear backup init flag") + } + + Buttons.LargeTonal( + onClick = { + SignalStore.backup.messageCredentials.clearAll() + SignalStore.backup.mediaCredentials.clearAll() + } + ) { + Text("Clear backup credentials") + } + + Dividers.Default() + Row( verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index c57309afed..b2eeafd9f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -475,14 +475,15 @@ class InternalBackupPlaygroundViewModel : ViewModel() { attachments: List = this.attachments, inProgress: Set = this.inProgressMediaIds ): MediaState { - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val updatedAttachments = attachments.map { val state = if (inProgress.contains(it.dbAttachment.attachmentId)) { BackupAttachment.State.IN_PROGRESS } else if (it.dbAttachment.archiveMediaName != null) { if (it.dbAttachment.remoteDigest != null) { - val mediaId = backupKey.deriveMediaId(MediaName(it.dbAttachment.archiveMediaName)).encode() + val mediaId = mediaRootBackupKey.deriveMediaId(MediaName(it.dbAttachment.archiveMediaName)).encode() if (it.dbAttachment.archiveMediaId == mediaId) { BackupAttachment.State.UPLOADED_FINAL } else { @@ -552,10 +553,10 @@ class InternalBackupPlaygroundViewModel : ViewModel() { val encryptedStream = tempBackupFile.inputStream() val iv = encryptedStream.readNBytesOrThrow(16) - val backupKey = SignalStore.svr.orCreateMasterKey.deriveBackupKey() + val backupKey = SignalStore.backup.messageBackupKey val keyMaterial = backupKey.deriveBackupSecrets(Recipient.self().aci.get()) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv)) + init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.aesKey, "AES"), IvParameterSpec(iv)) } val plaintextStream = GZIPInputStream( diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt index db4c2553ca..3ffc7f21bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt @@ -92,13 +92,13 @@ class ArchiveThumbnailUploadJob private constructor( return Result.success() } - val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey() + val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey val specResult = BackupRepository .getAttachmentUploadForm() .then { form -> SignalNetwork.attachments.getResumableUploadSpec( - key = backupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()), + key = mediaRootBackupKey.deriveThumbnailTransitKey(attachment.getThumbnailMediaName()), iv = attachment.remoteIv!!, uploadForm = form ) @@ -133,13 +133,12 @@ class ArchiveThumbnailUploadJob private constructor( return Result.retry(defaultBackoff()) } - val backupDirectories = BackupRepository.getCdnBackupDirectories().successOrThrow() - val mediaSecrets = backupKey.deriveMediaSecrets(attachment.getThumbnailMediaName()) + val mediaSecrets = mediaRootBackupKey.deriveMediaSecrets(attachment.getThumbnailMediaName()) return when (val result = BackupRepository.copyThumbnailToArchive(attachmentPointer, attachment)) { is NetworkResult.Success -> { // save attachment thumbnail - val archiveMediaId = attachment.archiveMediaId ?: backupKey.deriveMediaId(attachment.getMediaName()).encode() + val archiveMediaId = attachment.archiveMediaId ?: mediaRootBackupKey.deriveMediaId(attachment.getMediaName()).encode() SignalDatabase.attachments.finalizeAttachmentThumbnailAfterUpload(attachmentId, archiveMediaId, mediaSecrets.id, thumbnailResult.data) Log.d(TAG, "Successfully archived thumbnail for $attachmentId mediaName=${attachment.getThumbnailMediaName()}") 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 6ccf262717..0100e15802 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -225,7 +225,7 @@ class RestoreAttachmentJob private constructor( messageReceiver .retrieveArchivedAttachment( - SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(MediaName(attachment.archiveMediaName!!)), + SignalStore.backup.mediaRootBackupKey.deriveMediaSecrets(MediaName(attachment.archiveMediaName!!)), cdnCredentials, archiveFile, pointer, 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 f8b60468d8..71ad600161 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt @@ -123,7 +123,7 @@ class RestoreAttachmentThumbnailJob private constructor( Log.i(TAG, "Downloading thumbnail for $attachmentId") val downloadResult = AppDependencies.signalServiceMessageReceiver .retrieveArchivedAttachment( - SignalStore.svr.getOrCreateMasterKey().deriveBackupKey().deriveMediaSecrets(attachment.getThumbnailMediaName()), + SignalStore.backup.mediaRootBackupKey.deriveMediaSecrets(attachment.getThumbnailMediaName()), cdnCredentials, thumbnailTransferFile, pointer, diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index ef42c57a4f..86e60776d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -2,15 +2,20 @@ package org.thoughtcrime.securesms.keyvalue import com.fasterxml.jackson.annotation.JsonProperty import kotlinx.coroutines.flow.Flow +import okio.withLock import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.backup.RestoreState import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState +import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.internal.util.JsonUtil import java.io.IOException +import java.util.concurrent.locks.ReentrantLock import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours @@ -19,7 +24,8 @@ import kotlin.time.Duration.Companion.milliseconds class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { companion object { val TAG = Log.tag(BackupValues::class.java) - private const val KEY_CREDENTIALS = "backup.credentials" + private const val KEY_MESSAGE_CREDENTIALS = "backup.messageCredentials" + private const val KEY_MEDIA_CREDENTIALS = "backup.mediaCredentials" private const val KEY_CDN_READ_CREDENTIALS = "backup.cdn.readCredentials" private const val KEY_CDN_READ_CREDENTIALS_TIMESTAMP = "backup.cdn.readCredentials.timestamp" private const val KEY_RESTORE_STATE = "backup.restoreState" @@ -53,6 +59,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey" private val cachedCdnCredentialsExpiresIn: Duration = 12.hours + + private val lock = ReentrantLock() } override fun onFirstEverAppLaunch() = Unit @@ -74,7 +82,35 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1) var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer) - var mediaRootBackupKey: ByteArray? by nullableBlobValue(KEY_MEDIA_ROOT_BACKUP_KEY, null) + /** + * Key used to backup messages. + */ + val messageBackupKey: MessageBackupKey + get() = SignalStore.svr.getOrCreateMasterKey().derivateMessageBackupKey() + + /** + * Key used to backup media. Purely random and separate from the message backup key. + */ + var mediaRootBackupKey: MediaRootBackupKey + get() { + lock.withLock { + val value: ByteArray? = getBlob(KEY_MEDIA_ROOT_BACKUP_KEY, null) + if (value != null) { + return MediaRootBackupKey(value) + } + + Log.i(TAG, "Generating MediaRootBackupKey...", Throwable()) + val bytes = Util.getSecretBytes(32) + putBlob(KEY_MEDIA_ROOT_BACKUP_KEY, bytes) + return MediaRootBackupKey(bytes) + } + } + set(value) { + lock.withLock { + Log.i(TAG, "Setting MediaRootBackupKey", Throwable()) + putBlob(KEY_MEDIA_ROOT_BACKUP_KEY, value.value) + } + } /** * This is the 'latest' backup tier. This isn't necessarily the user's current backup tier, so this should only ever @@ -153,24 +189,11 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { val isRestoreInProgress: Boolean get() = totalRestorableAttachmentSize > 0 - /** - * Retrieves the stored credentials, mapped by the day they're valid. The day is represented as - * the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials] - * type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime]. - */ - val credentialsByDay: ArchiveServiceCredentials - get() { - val serialized = store.getString(KEY_CREDENTIALS, null) ?: return ArchiveServiceCredentials() + /** Store that lets you interact with message ZK credentials. */ + val messageCredentials = CredentialStore(KEY_MESSAGE_CREDENTIALS) - return try { - val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay - ArchiveServiceCredentials(map) - } catch (e: IOException) { - Log.w(TAG, "Invalid JSON! Clearing.", e) - putString(KEY_CREDENTIALS, null) - ArchiveServiceCredentials() - } - } + /** Store that lets you interact with media ZK credentials. */ + val mediaCredentials = CredentialStore(KEY_MEDIA_CREDENTIALS) var cdnReadCredentials: GetArchiveCdnCredentialsResponse? get() { @@ -194,26 +217,44 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { cachedCdnCredentialsTimestamp = System.currentTimeMillis() } - /** - * Adds the given credentials to the existing list of stored credentials. - */ - fun addCredentials(credentials: List) { - val current: MutableMap = credentialsByDay.toMutableMap() - current.putAll(credentials.associateBy { it.redemptionTime }) - putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(current))) - } + inner class CredentialStore(val key: String) { + /** + * Retrieves the stored media credentials, mapped by the day they're valid. The day is represented as + * the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials] + * type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime]. + */ + val byDay: ArchiveServiceCredentials + get() { + val serialized = store.getString(key, null) ?: return ArchiveServiceCredentials() - /** - * Trims out any credentials that are for days older than the given timestamp. - */ - fun clearCredentialsOlderThan(startOfDayInSeconds: Long) { - val current: MutableMap = credentialsByDay.toMutableMap() - val updated = current.filterKeys { it < startOfDayInSeconds } - putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(updated))) - } + return try { + val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay + ArchiveServiceCredentials(map) + } catch (e: IOException) { + Log.w(TAG, "Invalid JSON! Clearing.", e) + putString(key, null) + ArchiveServiceCredentials() + } + } - fun clearAllCredentials() { - putString(KEY_CREDENTIALS, null) + /** Adds the given credentials to the existing list of stored credentials. */ + fun add(credentials: List) { + val current: MutableMap = byDay.toMutableMap() + current.putAll(credentials.associateBy { it.redemptionTime }) + putString(key, JsonUtil.toJson(SerializedCredentials(current))) + } + + /** Trims out any credentials that are for days older than the given timestamp. */ + fun clearOlderThan(startOfDayInSeconds: Long) { + val current: MutableMap = byDay.toMutableMap() + val updated = current.filterKeys { it < startOfDayInSeconds } + putString(key, JsonUtil.toJson(SerializedCredentials(updated))) + } + + /** Clears all credentials. */ + fun clearAll() { + putString(key, null) + } } fun markMessageBackupFailure() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt index 4e64a18f4f..fdd6930c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceRepository.kt @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher import org.whispersystems.signalservice.api.NetworkResult -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo @@ -115,9 +115,9 @@ object LinkDeviceRepository { /** * Adds a linked device to the account. * - * @param ephemeralBackupKey An ephemeral key to provide the linked device to sync existing message content. Do not set if link+sync is unsupported. + * @param ephemeralMessageBackupKey An ephemeral key to provide the linked device to sync existing message content. Do not set if link+sync is unsupported. */ - fun addDevice(uri: Uri, ephemeralBackupKey: BackupKey?): LinkDeviceResult { + fun addDevice(uri: Uri, ephemeralMessageBackupKey: MessageBackupKey?): LinkDeviceResult { if (!isValidQr(uri)) { Log.w(TAG, "Bad URI! $uri") return LinkDeviceResult.BadCode @@ -155,7 +155,7 @@ object LinkDeviceRepository { profileKey = ProfileKeyUtil.getSelfProfileKey(), masterKey = SignalStore.svr.getOrCreateMasterKey(), code = verificationCodeResult.verificationCode, - ephemeralBackupKey = ephemeralBackupKey + ephemeralMessageBackupKey = ephemeralMessageBackupKey ) return when (deviceLinkResult) { @@ -227,13 +227,13 @@ object LinkDeviceRepository { /** * Performs the entire process of creating and uploading an archive for a newly-linked device. */ - fun createAndUploadArchive(ephemeralBackupKey: BackupKey, deviceId: Int, deviceCreatedAt: Long): LinkUploadArchiveResult { + fun createAndUploadArchive(ephemeralMessageBackupKey: MessageBackupKey, deviceId: Int, deviceCreatedAt: Long): LinkUploadArchiveResult { val stopwatch = Stopwatch("link-archive") val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application) val outputStream = FileOutputStream(tempBackupFile) try { - BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, backupKey = ephemeralBackupKey, mediaBackupEnabled = false) + BackupRepository.export(outputStream = outputStream, append = { tempBackupFile.appendBytes(it) }, messageBackupKey = ephemeralMessageBackupKey, mediaBackupEnabled = false) } catch (e: Exception) { return LinkUploadArchiveResult.BackupCreationFailure(e) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt index 608933edc3..0257728ae8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt @@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.OneTimeEven import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.QrCodeState import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse import kotlin.time.Duration.Companion.seconds @@ -198,8 +198,8 @@ class LinkDeviceViewModel : ViewModel() { } private fun addDeviceWithSync(linkUri: Uri) { - val ephemeralBackupKey = BackupKey(Util.getSecretBytes(32)) - val result = LinkDeviceRepository.addDevice(linkUri, ephemeralBackupKey) + val ephemeralMessageBackupKey = MessageBackupKey(Util.getSecretBytes(32)) + val result = LinkDeviceRepository.addDevice(linkUri, ephemeralMessageBackupKey) _state.update { it.copy( @@ -235,7 +235,7 @@ class LinkDeviceViewModel : ViewModel() { } Log.i(TAG, "Beginning the archive generation process...") - val uploadResult = LinkDeviceRepository.createAndUploadArchive(ephemeralBackupKey, waitResult.id, waitResult.created) + val uploadResult = LinkDeviceRepository.createAndUploadArchive(ephemeralMessageBackupKey, waitResult.id, waitResult.created) when (uploadResult) { LinkDeviceRepository.LinkUploadArchiveResult.Success -> { _state.update { @@ -258,7 +258,7 @@ class LinkDeviceViewModel : ViewModel() { } private fun addDeviceWithoutSync(linkUri: Uri) { - val result = LinkDeviceRepository.addDevice(linkUri, ephemeralBackupKey = null) + val result = LinkDeviceRepository.addDevice(linkUri, ephemeralMessageBackupKey = null) _state.update { it.copy( 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 f33e153a66..1b4046e93b 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 @@ -19,8 +19,7 @@ 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.backup.MediaRootBackupKey; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; @@ -72,8 +71,8 @@ class PartDataSource implements DataSource { 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)); + MediaRootBackupKey.MediaKeyMaterial mediaKeyMaterial = SignalStore.backup().getMediaRootBackupKey().deriveMediaSecretsFromMediaId(attachment.archiveMediaId); + long originalCipherLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size)); this.inputStream = AttachmentCipherInputStream.createStreamingForArchivedAttachment(mediaKeyMaterial, archiveFile, originalCipherLength, attachment.size, attachment.remoteDigest, decode, attachment.getIncrementalDigest(), attachment.incrementalMacChunkSize); } catch (InvalidMessageException e) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt index f9849b3b61..cf8f22dee2 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import java.io.ByteArrayOutputStream import java.util.UUID @@ -22,7 +22,7 @@ class EncryptedBackupReaderWriterTest { @Test fun `can read back all of the frames we write`() { - val key = BackupKey(Util.getSecretBytes(32)) + val key = MessageBackupKey(Util.getSecretBytes(32)) val aci = ACI.from(UUID.randomUUID()) val outputStream = ByteArrayOutputStream() @@ -54,7 +54,7 @@ class EncryptedBackupReaderWriterTest { @Test fun `padding limits number of sizes`() { - val key = BackupKey(Util.getSecretBytes(32)) + val key = MessageBackupKey(Util.getSecretBytes(32)) val aci = ACI.from(UUID.randomUUID()) val uniqueSizes = (1..10) @@ -78,7 +78,7 @@ class EncryptedBackupReaderWriterTest { @Test fun `using a different IV every time`() { - val key = BackupKey(Util.getSecretBytes(32)) + val key = MessageBackupKey(Util.getSecretBytes(32)) val aci = ACI.from(UUID.randomUUID()) val count = 10 diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 0b07ffafa1..2262590630 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -15,7 +15,7 @@ dependencyResolutionManagement { version("exoplayer", "2.19.0") version("glide", "4.15.1") version("kotlin", "1.9.20") - version("libsignal-client", "0.60.0") + version("libsignal-client", "0.60.1") version("mp4parser", "1.9.39") version("android-gradle-plugin", "8.4.0") version("accompanist", "0.28.0") diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a1ef5f6c0e..2c46272f88 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -9013,28 +9013,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + - - - + + + - - - - - - - - - - + + 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 222dd729db..d7c6431d65 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 @@ -12,10 +12,9 @@ import org.signal.core.util.concurrent.ListenableFuture; import org.signal.core.util.concurrent.SettableFuture; import org.signal.core.util.stream.LimitedInputStream; 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.backup.MediaRootBackupKey; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil; import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; @@ -29,9 +28,7 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; -import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.internal.ServiceResponse; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.IdentityCheckRequest; import org.whispersystems.signalservice.internal.push.IdentityCheckResponse; @@ -187,7 +184,7 @@ public class SignalServiceMessageReceiver { * * @return An InputStream that streams the plaintext attachment contents. */ - public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, + public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial, @Nonnull Map readCredentialHeaders, @Nonnull File archiveDestination, @Nonnull SignalServiceAttachmentPointer pointer, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt index 867ebd8bf6..c399896b83 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt @@ -14,6 +14,8 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse.StoredMediaObject import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.internal.push.AttachmentUploadForm import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -44,6 +46,13 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * credentials is to keep the caller anonymous, but that doesn't help if this authenticated request * happens right before all of the unauthenticated ones, as that would make it easier to correlate * traffic. + * + * GET /v1/archives/auth + * + * - 200: Success + * - 400: Bad start/end times + * - 404: BackupId could not be found + * - 429: Rate-limited */ fun getServiceCredentials(currentTime: Long): NetworkResult { return NetworkResult.fromFetch { @@ -51,30 +60,46 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { } } - fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getCdnReadCredentials(cdnNumber: Int, messageBackupKey: MessageBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(messageBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation()) } } /** - * Ensures that you reserve a backupId on the service. This must be done before any other + * Ensures that you reserve backupIds for both messages and media on the service. This must be done before any other * backup-related calls. You only need to do it once, but repeated calls are safe. + * + * PUT /v1/archives/backupid + * + * - 204: Success + * - 400: Invalid credential + * - 429: Rate-limited + * */ - fun triggerBackupIdReservation(backupKey: BackupKey, aci: ACI): NetworkResult { + fun triggerBackupIdReservation(messageBackupKey: MessageBackupKey, mediaRootBackupKey: MediaRootBackupKey, aci: ACI): NetworkResult { return NetworkResult.fromFetch { - val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) - pushServiceSocket.setArchiveBackupId(backupRequestContext.request) + val messageBackupRequestContext = BackupAuthCredentialRequestContext.create(messageBackupKey.value, aci.rawUuid) + val mediaBackupRequestContext = BackupAuthCredentialRequestContext.create(mediaRootBackupKey.value, aci.rawUuid) + pushServiceSocket.setArchiveBackupId(messageBackupRequestContext.request, mediaBackupRequestContext.request) } } /** - * Sets a public key on the service derived from your [BackupKey]. This key is used to prevent + * Sets a public key on the service derived from your [MessageBackupKey]. This key is used to prevent * unauthorized users from changing your backup data. You only need to do it once, but repeated * calls are safe. + * + * PUT /v1/archives/keys + * + * - 204: Success + * - 400: Bad arguments, or request was made on an authenticated channel + * - 401: Bad presentation, invalid public key signature, no matching backupId on teh server, or the credential was of the wrong type (messages/media) + * - 403: Forbidden + * - 429: Rate-limited */ fun setPublicKey(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { @@ -87,16 +112,16 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { /** * Fetches an upload form you can use to upload your main message backup file to cloud storage. * - * Responses - * 200: Success - * 400: Bad args, or made on an authenticated channel - * 403: Insufficient permissions - * 429: Rate-limited + * GET /v1/archives/upload/form + * - 200: Success + * - 400: Bad args, or made on an authenticated channel + * - 403: Insufficient permissions + * - 429: Rate-limited */ - fun getMessageBackupUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getMessageBackupUploadForm(messageBackupKey: MessageBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(messageBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation()) } } @@ -106,10 +131,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a * backup yet. */ - fun getBackupInfo(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getBackupInfo(messageBackupKey: MessageBackupKey, aci: ACI, messageServiceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(messageBackupKey, aci, messageServiceCredential) + val presentationData = CredentialPresentationData.from(messageBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation()) } } @@ -117,10 +142,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { /** * Lists the media objects in the backup */ - fun listMediaObjects(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { + fun listMediaObjects(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) } } @@ -150,11 +175,18 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * so we can request them more often (which is required for backfilling). * * After uploading, the media still needs to be copied via [copyAttachmentToArchive]. + * + * GET /v1/archives/media/upload/form + * + * - 200: Success + * - 400: Bad request, or made on authenticated channel + * - 403: Forbidden + * - 429: Rate-limited */ - fun getMediaUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getMediaUploadForm(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation()) } } @@ -163,13 +195,13 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * Retrieves all media items in the user's archive. Note that this could be a very large number of items, making this only suitable for debugging. * Use [getArchiveMediaItemsPage] in production. */ - fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult> { + fun debugGetUploadedMediaItemMetadata(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult> { return NetworkResult.fromFetch { val mediaObjects: MutableList = ArrayList() var cursor: String? = null do { - val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, aci, serviceCredential, 512, cursor).successOrThrow() + val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(mediaRootBackupKey, aci, serviceCredential, 512, cursor).successOrThrow() mediaObjects += response.storedMediaObjects cursor = response.cursor } while (cursor != null) @@ -183,10 +215,10 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * @param limit The maximum number of items to return. * @param cursor A token that can be read from your previous response, telling the server where to start the next page. */ - fun getArchiveMediaItemsPage(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { + fun getArchiveMediaItemsPage(mediaRootBackupKey: MediaRootBackupKey, aci: ACI, mediaServiceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, mediaServiceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) } @@ -204,14 +236,14 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * 429: Rate-limited */ fun copyAttachmentToArchive( - backupKey: BackupKey, + mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, item: ArchiveMediaRequest ): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item) } @@ -221,14 +253,14 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { * Copy and re-encrypt media from the attachments cdn into the backup cdn. */ fun copyAttachmentToArchive( - backupKey: BackupKey, + mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, items: List ): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) val request = BatchArchiveMediaRequest(items = items) @@ -238,16 +270,23 @@ class ArchiveApi(private val pushServiceSocket: PushServiceSocket) { /** * Delete media from the backup cdn. + * + * POST /v1/archives/media/delete + * + * - 400: Bad args or made on an authenticated channel + * - 401: Bad presentation, invalid public key signature, no matching backupId on teh server, or the credential was of the wrong type (messages/media) + * - 403: Forbidden + * - 429: Rate-limited */ fun deleteArchivedMedia( - backupKey: BackupKey, + mediaRootBackupKey: MediaRootBackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, mediaToDelete: List ): NetworkResult { return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, aci, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) + val zkCredential = getZkCredential(mediaRootBackupKey, aci, serviceCredential) + val presentationData = CredentialPresentationData.from(mediaRootBackupKey, aci, zkCredential, backupServerPublicParams) val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete) pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMediaRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMediaRequest.kt index 3ed0a8c00d..1232e61983 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMediaRequest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMediaRequest.kt @@ -15,8 +15,7 @@ class ArchiveMediaRequest( @JsonProperty val objectLength: Int, @JsonProperty val mediaId: String, @JsonProperty val hmacKey: String, - @JsonProperty val encryptionKey: String, - @JsonProperty val iv: String + @JsonProperty val encryptionKey: String ) { class SourceAttachment( @JsonProperty val cdn: Int, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialPair.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialPair.kt new file mode 100644 index 0000000000..6cee293762 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialPair.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +/** + * A convenient container for passing around both a message and media archive service credential. + */ +data class ArchiveServiceCredentialPair( + val messageCredential: ArchiveServiceCredential, + val mediaCredential: ArchiveServiceCredential +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt index bb470f1c28..0d047225be 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt @@ -6,6 +6,7 @@ package org.whispersystems.signalservice.api.archive import com.fasterxml.jackson.annotation.JsonProperty +import okio.IOException /** * Represents the result of fetching archive credentials. @@ -13,5 +14,26 @@ import com.fasterxml.jackson.annotation.JsonProperty */ class ArchiveServiceCredentialsResponse( @JsonProperty - val credentials: Array -) + val credentials: Map> +) { + companion object { + private const val KEY_MESSAGES = "messages" + private const val KEY_MEDIA = "media" + } + + init { + if (!credentials.containsKey(KEY_MESSAGES)) { + throw IOException("Missing key '$KEY_MESSAGES'") + } + + if (!credentials.containsKey(KEY_MEDIA)) { + throw IOException("Missing key '$KEY_MEDIA'") + } + } + + val messageCredentials: List + get() = credentials[KEY_MESSAGES]!! + + val mediaCredentials: List + get() = credentials[KEY_MEDIA]!! +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt index 38a50c9474..22c35b687c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt @@ -19,7 +19,10 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest class ArchiveSetBackupIdRequest( @JsonProperty @JsonSerialize(using = BackupAuthCredentialRequestSerializer::class) - val backupAuthCredentialRequest: BackupAuthCredentialRequest + val messagesBackupAuthCredentialRequest: BackupAuthCredentialRequest, + @JsonProperty + @JsonSerialize(using = BackupAuthCredentialRequestSerializer::class) + val mediaBackupAuthCredentialRequest: BackupAuthCredentialRequest ) { class BackupAuthCredentialRequestSerializer : JsonSerializer() { override fun serialize(value: BackupAuthCredentialRequest, gen: JsonGenerator, serializers: SerializerProvider) { 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 8b81a73ef0..1b7f961cf7 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 @@ -1,104 +1,22 @@ /* - * Copyright 2023 Signal Messenger, LLC + * Copyright 2024 Signal Messenger, LLC * SPDX-License-Identifier: AGPL-3.0-only */ package org.whispersystems.signalservice.api.backup -import org.signal.libsignal.protocol.ecc.Curve import org.signal.libsignal.protocol.ecc.ECPrivateKey -import org.signal.libsignal.protocol.kdf.HKDF import org.whispersystems.signalservice.api.push.ServiceId.ACI /** - * Safe typing around a backup key, which is a 32-byte array. + * Contains the common properties for all "backup keys", namely the [MessageBackupKey] and [MediaRootBackupKey] */ -class BackupKey(val value: ByteArray) { - init { - require(value.size == 32) { "Backup key must be 32 bytes!" } - } +interface BackupKey { - /** - * Identifies a the location of a user's backup. - */ - fun deriveBackupId(aci: ACI): BackupId { - return BackupId( - HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupId".toByteArray(), 16) - ) - } - - /** - * The cryptographic material used to encrypt a backup. - */ - fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial { - val backupId = deriveBackupId(aci) - - val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80) - - return BackupKeyMaterial( - id = backupId, - macKey = extendedKey.copyOfRange(0, 32), - cipherKey = extendedKey.copyOfRange(32, 64) - ) - } + val value: ByteArray /** * The private key used to generate anonymous credentials when interacting with the backup service. */ - fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey { - val material = HKDF.deriveSecrets(this.value, aci.toByteArray(), "20231003_Signal_Backups_GenerateBackupIdKeyPair".toByteArray(), 32) - return Curve.decodePrivatePoint(material) - } - - fun deriveMediaId(mediaName: MediaName): MediaId { - return MediaId(HKDF.deriveSecrets(value, mediaName.toByteArray(), "20231003_Signal_Backups_Media_ID".toByteArray(), 15)) - } - - fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial { - 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) - } - - private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial { - val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80) - - return MediaKeyMaterial( - id = mediaId, - macKey = extendedKey.copyOfRange(0, 32), - cipherKey = extendedKey.copyOfRange(32, 64), - iv = extendedKey.copyOfRange(64, 80) - ) - } - - class BackupKeyMaterial( - val id: BackupId, - val macKey: ByteArray, - val cipherKey: ByteArray - ) - - class MediaKeyMaterial( - val id: MediaId, - val macKey: ByteArray, - val cipherKey: ByteArray, - val iv: ByteArray - ) { - companion object { - @JvmStatic - fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): MediaKeyMaterial { - return MediaKeyMaterial( - MediaId(id), - keyMac.copyOfRange(32, 64), - keyMac.copyOfRange(0, 32), - iv - ) - } - } - } + fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt new file mode 100644 index 0000000000..84cd7cf22b --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MediaRootBackupKey.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.backup + +import org.signal.libsignal.protocol.ecc.ECPrivateKey +import org.signal.libsignal.protocol.kdf.HKDF +import org.whispersystems.signalservice.api.push.ServiceId.ACI +import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey + +/** + * Safe typing around a media root backup key, which is a 32-byte array. + * This key is a purely random value. + */ +class MediaRootBackupKey(override val value: ByteArray) : BackupKey { + + /** + * The private key used to generate anonymous credentials when interacting with the backup service. + */ + override fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey { + return LibSignalBackupKey(value).deriveEcKey(aci.libSignalAci) + } + + fun deriveMediaId(mediaName: MediaName): MediaId { + return MediaId(LibSignalBackupKey(value).deriveMediaId(mediaName.name)) + } + + fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial { + val mediaId = deriveMediaId(mediaName) + return deriveMediaSecrets(mediaId) + } + + 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) + } + + private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial { + val libsignalBackupKey = LibSignalBackupKey(value) + val combinedKey = libsignalBackupKey.deriveMediaEncryptionKey(mediaId.value) + + return MediaKeyMaterial( + id = mediaId, + macKey = combinedKey.copyOfRange(0, 32), + aesKey = combinedKey.copyOfRange(32, 64) + ) + } + + class MediaKeyMaterial( + val id: MediaId, + val macKey: ByteArray, + val aesKey: ByteArray + ) +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MessageBackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MessageBackupKey.kt new file mode 100644 index 0000000000..e9c4980d63 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/MessageBackupKey.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.backup + +import org.signal.libsignal.protocol.ecc.ECPrivateKey +import org.whispersystems.signalservice.api.push.ServiceId.ACI +import org.signal.libsignal.messagebackup.BackupKey as LibSignalBackupKey +import org.signal.libsignal.messagebackup.MessageBackupKey as LibSignalMessageBackupKey + +/** + * Safe typing around a backup key, which is a 32-byte array. + * This key is derived from the master key. + */ +class MessageBackupKey(override val value: ByteArray) : BackupKey { + init { + require(value.size == 32) { "Backup key must be 32 bytes!" } + } + + /** + * The cryptographic material used to encrypt a backup. + */ + fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial { + val backupId = deriveBackupId(aci) + val libsignalBackupKey = LibSignalBackupKey(value) + val libsignalMessageMessageBackupKey = LibSignalMessageBackupKey(libsignalBackupKey, backupId.value) + + return BackupKeyMaterial( + id = backupId, + macKey = libsignalMessageMessageBackupKey.hmacKey, + aesKey = libsignalMessageMessageBackupKey.aesKey + ) + } + + /** + * The private key used to generate anonymous credentials when interacting with the backup service. + */ + override fun deriveAnonymousCredentialPrivateKey(aci: ACI): ECPrivateKey { + return LibSignalBackupKey(value).deriveEcKey(aci.libSignalAci) + } + + /** + * Identifies a the location of a user's backup. + */ + private fun deriveBackupId(aci: ACI): BackupId { + return BackupId( + LibSignalBackupKey(value).deriveBackupId(aci.libSignalAci) + ) + } + + class BackupKeyMaterial( + val id: BackupId, + val macKey: ByteArray, + val aesKey: ByteArray + ) +} 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 2c279d7d70..5d099238c8 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 @@ -11,7 +11,7 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice; import org.signal.libsignal.protocol.incrementalmac.IncrementalMacInputStream; import org.signal.libsignal.protocol.kdf.HKDF; -import org.whispersystems.signalservice.api.backup.BackupKey; +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey; import org.whispersystems.signalservice.internal.util.Util; import java.io.ByteArrayInputStream; @@ -126,7 +126,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { /** * Decrypt archived media to it's original attachment encrypted blob. */ - public static LimitedInputStream createForArchivedMedia(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength) + public static LimitedInputStream createForArchivedMedia(MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength) throws InvalidMessageException, IOException { Mac mac = initMac(archivedMediaKeyMaterial.getMacKey()); @@ -139,7 +139,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { verifyMac(macVerificationStream, file.length(), mac, null); } - InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), archivedMediaKeyMaterial.getCipherKey(), file.length() - BLOCK_SIZE - mac.getMacLength()); + InputStream inputStream = new AttachmentCipherInputStream(new FileInputStream(file), archivedMediaKeyMaterial.getAesKey(), file.length() - BLOCK_SIZE - mac.getMacLength()); if (originalCipherTextLength != 0) { return new LimitedInputStream(inputStream, originalCipherTextLength); @@ -148,7 +148,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { } } - public static LimitedInputStream createStreamingForArchivedAttachment(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength, long plaintextLength, byte[] combinedKeyMaterial, byte[] digest, byte[] incrementalDigest, int incrementalMacChunkSize) + public static LimitedInputStream createStreamingForArchivedAttachment(MediaRootBackupKey.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); @@ -204,7 +204,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { return new AttachmentCipherInputStream(new ByteArrayInputStream(data), parts[0], data.length - BLOCK_SIZE - mac.getMacLength()); } - private AttachmentCipherInputStream(InputStream inputStream, byte[] cipherKey, long totalDataSize) + private AttachmentCipherInputStream(InputStream inputStream, byte[] aesKey, long totalDataSize) throws IOException { super(inputStream); @@ -214,7 +214,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { readFully(iv); this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); + this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new IvParameterSpec(iv)); this.done = false; this.totalRead = 0; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java index 68a673acee..950b645578 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/kbs/MasterKey.java @@ -1,7 +1,7 @@ package org.whispersystems.signalservice.api.kbs; import org.signal.libsignal.protocol.kdf.HKDF; -import org.whispersystems.signalservice.api.backup.BackupKey; +import org.whispersystems.signalservice.api.backup.MessageBackupKey; import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.internal.util.Hex; import org.signal.core.util.Base64; @@ -46,8 +46,9 @@ public final class MasterKey { return derive("Logging Key"); } - public BackupKey deriveBackupKey() { - return new BackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32)); + public MessageBackupKey derivateMessageBackupKey() { + // TODO [backup] Derive from AEP + return new MessageBackupKey(HKDF.deriveSecrets(masterKey, "20231003_Signal_Backups_GenerateBackupKey".getBytes(), 32)); } private byte[] derive(String keyName) { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt index d7eb52b91e..7dfa25b218 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/link/LinkDeviceApi.kt @@ -10,7 +10,7 @@ import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.ecc.ECPublicKey import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.whispersystems.signalservice.api.NetworkResult -import org.whispersystems.signalservice.api.backup.BackupKey +import org.whispersystems.signalservice.api.backup.MessageBackupKey import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI @@ -63,7 +63,7 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) { profileKey: ProfileKey, masterKey: MasterKey, code: String, - ephemeralBackupKey: BackupKey? + ephemeralMessageBackupKey: MessageBackupKey? ): NetworkResult { return NetworkResult.fromFetch { val cipher = PrimaryProvisioningCipher(deviceKey) @@ -79,7 +79,7 @@ class LinkDeviceApi(private val pushServiceSocket: PushServiceSocket) { provisioningCode = code, provisioningVersion = ProvisioningVersion.CURRENT.value, masterKey = masterKey.serialize().toByteString(), - ephemeralBackupKey = ephemeralBackupKey?.value?.toByteString() + ephemeralBackupKey = ephemeralMessageBackupKey?.value?.toByteString() ) val ciphertext = cipher.encrypt(message) 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 c9884c35d1..9af8f8b431 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 @@ -540,8 +540,8 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class); } - public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException { - String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request)); + public void setArchiveBackupId(BackupAuthCredentialRequest messageRequest, BackupAuthCredentialRequest mediaRequest) throws IOException { + String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(messageRequest, mediaRequest)); makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); } diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java index 0fdfda0afb..f11f1d0ed0 100644 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java @@ -7,7 +7,7 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice; import org.signal.libsignal.protocol.incrementalmac.InvalidMacException; import org.signal.libsignal.protocol.kdf.HKDFv3; -import org.whispersystems.signalservice.api.backup.BackupKey; +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey; import org.whispersystems.signalservice.internal.crypto.PaddingInputStream; import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.util.Util; @@ -290,13 +290,13 @@ public final class AttachmentCipherTest { @Test public void archive_encryptDecrypt() throws IOException, InvalidMessageException { - byte[] key = Util.getSecretBytes(64); - BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); - byte[] plaintextInput = "Peter Parker".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key, false); - File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); - byte[] plaintextOutput = readInputStreamFully(inputStream); + byte[] key = Util.getSecretBytes(64); + MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key); + byte[] plaintextInput = "Peter Parker".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, false); + File cipherFile = writeToFile(encryptResult.ciphertext); + InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); + byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -305,13 +305,13 @@ public final class AttachmentCipherTest { @Test public void archive_encryptDecryptEmpty() throws IOException, InvalidMessageException { - byte[] key = Util.getSecretBytes(64); - BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); - byte[] plaintextInput = "".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key, false); - File cipherFile = writeToFile(encryptResult.ciphertext); - InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); - byte[] plaintextOutput = readInputStreamFully(inputStream); + byte[] key = Util.getSecretBytes(64); + MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key); + byte[] plaintextInput = "".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, false); + File cipherFile = writeToFile(encryptResult.ciphertext); + InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); + byte[] plaintextOutput = readInputStreamFully(inputStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -324,11 +324,11 @@ public final class AttachmentCipherTest { boolean hitCorrectException = false; try { - byte[] key = Util.getSecretBytes(64); - byte[] badKey = Util.getSecretBytes(64); - BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16)); - byte[] plaintextInput = "Gwen Stacy".getBytes(); - EncryptResult encryptResult = encryptData(plaintextInput, key, false); + byte[] key = Util.getSecretBytes(64); + byte[] badKey = Util.getSecretBytes(64); + MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(badKey); + byte[] plaintextInput = "Gwen Stacy".getBytes(); + EncryptResult encryptResult = encryptData(plaintextInput, key, false); cipherFile = writeToFile(encryptResult.ciphertext); @@ -372,9 +372,9 @@ public final class AttachmentCipherTest { File cipherFile = writeToFile(encryptedData); - BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); - InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length); - byte[] plaintextOutput = readInputStreamFully(decryptedStream); + MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key); + InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length); + byte[] plaintextOutput = readInputStreamFully(decryptedStream); assertArrayEquals(plaintextInput, plaintextOutput); @@ -397,7 +397,7 @@ public final class AttachmentCipherTest { cipherFile = writeToFile(badMacCiphertext); - BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); + MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key); AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); fail(); } catch (InvalidMessageException e) { diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTestHelper.kt b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTestHelper.kt new file mode 100644 index 0000000000..40e3a319d4 --- /dev/null +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTestHelper.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.crypto + +import org.whispersystems.signalservice.api.backup.MediaId +import org.whispersystems.signalservice.api.backup.MediaRootBackupKey.MediaKeyMaterial +import org.whispersystems.signalservice.internal.util.Util + +object AttachmentCipherTestHelper { + + /** + * Needed to workaround this bug: + * https://youtrack.jetbrains.com/issue/KT-60205/Java-class-has-private-access-in-class-constructor-with-inlinevalue-parameter + */ + @JvmStatic + fun createMediaKeyMaterial(combinedKey: ByteArray): MediaKeyMaterial { + val parts = Util.split(combinedKey, 32, 32) + + return MediaKeyMaterial( + id = MediaId(Util.getSecretBytes(15)), + macKey = parts[1], + aesKey = parts[0] + ) + } +}