From 54e61796b3830d2b4d97e9b50c5429b6a5e27e06 Mon Sep 17 00:00:00 2001 From: S1m Date: Sun, 8 Sep 2024 16:59:58 +0000 Subject: [PATCH] Prepare local backup v2 --- .../securesms/backup/v2/BackupRepository.kt | 8 + .../api/SignalServiceAccountManager.java | 3 +- .../signalservice/api/archive/ArchiveApi.kt | 200 ++---------- .../api/archive/LocalArchiveApi.kt | 198 ++++++++++++ .../api/archive/RemoteArchiveApi.kt | 297 ++++++++++++++++++ 5 files changed, 523 insertions(+), 183 deletions(-) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/LocalArchiveApi.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/RemoteArchiveApi.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 d78263f58e..edf226ce19 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 @@ -485,6 +485,10 @@ object BackupRepository { .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .map { pair -> val (cdnCredentials, info) = pair + if (info.cdn!! < 0) { + //TODO local logic to download backup file + return@map NetworkResult.Success(Unit) + } val messageReceiver = AppDependencies.signalServiceMessageReceiver messageReceiver.retrieveBackup(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}", destination, listener) } is NetworkResult.Success @@ -501,6 +505,10 @@ object BackupRepository { .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .then { pair -> val (cdnCredentials, info) = pair + if (info.cdn!! < 0) { + //TODO local logic to get last modification time -> change now() + return@then NetworkResult.Success(ZonedDateTime.now()) + } val messageReceiver = AppDependencies.signalServiceMessageReceiver NetworkResult.fromFetch { messageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 5b7daf65f0..3fac380179 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -30,6 +30,7 @@ import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.archive.ArchiveApi; +import org.whispersystems.signalservice.api.archive.RemoteArchiveApi; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; @@ -1052,7 +1053,7 @@ public GroupsV2Api getGroupsV2Api() { } public ArchiveApi getArchiveApi() { - return ArchiveApi.create(pushServiceSocket, configuration.getBackupServerPublicParams(), credentials.getAci()); + return RemoteArchiveApi.create(pushServiceSocket, configuration.getBackupServerPublicParams(), credentials.getAci()); } public KeysApi getKeysApi() { 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 e05d72eb40..d6fcc986b8 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 @@ -5,42 +5,19 @@ package org.whispersystems.signalservice.api.archive -import org.signal.libsignal.protocol.ecc.Curve -import org.signal.libsignal.protocol.ecc.ECPrivateKey -import org.signal.libsignal.protocol.ecc.ECPublicKey -import org.signal.libsignal.zkgroup.GenericServerPublicParams import org.signal.libsignal.zkgroup.backups.BackupAuthCredential -import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequestContext -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.push.ServiceId.ACI import org.whispersystems.signalservice.internal.push.AttachmentUploadForm -import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec import java.io.InputStream -import java.time.Instant /** * Class to interact with various archive-related endpoints. * Why is it called archive instead of backup? Because SVR took the "backup" endpoint namespace first :) */ -class ArchiveApi( - private val pushServiceSocket: PushServiceSocket, - private val backupServerPublicParams: GenericServerPublicParams, - private val aci: ACI -) { - companion object { - @JvmStatic - fun create(pushServiceSocket: PushServiceSocket, backupServerPublicParams: ByteArray, aci: ACI): ArchiveApi { - return ArchiveApi( - pushServiceSocket, - GenericServerPublicParams(backupServerPublicParams), - aci - ) - } - } +interface ArchiveApi { /** * Retrieves a set of credentials one can use to authorize other requests. @@ -52,152 +29,69 @@ class ArchiveApi( * happens right before all of the unauthenticated ones, as that would make it easier to correlate * traffic. */ - fun getServiceCredentials(currentTime: Long): NetworkResult { - return NetworkResult.fromFetch { - pushServiceSocket.getArchiveCredentials(currentTime) - } - } + fun getServiceCredentials(currentTime: Long): NetworkResult - fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - - pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation()) - } - } + fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult /** * Ensures that you reserve a backupId 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. */ - fun triggerBackupIdReservation(backupKey: BackupKey): NetworkResult { - return NetworkResult.fromFetch { - val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) - pushServiceSocket.setArchiveBackupId(backupRequestContext.request) - } - } + fun triggerBackupIdReservation(backupKey: BackupKey): NetworkResult /** * Sets a public key on the service derived from your [BackupKey]. 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. */ - fun setPublicKey(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - pushServiceSocket.setArchivePublicKey(presentationData.publicKey, presentationData.toArchiveCredentialPresentation()) - } - } + fun setPublicKey(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult /** * Fetches an upload form you can use to upload your main message backup file to cloud storage. */ - fun getMessageBackupUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation()) - } - } + fun getMessageBackupUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult /** * Fetches metadata about your current backup. * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a * backup yet. */ - fun getBackupInfo(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation()) - } - } + fun getBackupInfo(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult /** * Lists the media objects in the backup */ - fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) - } - } + fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult /** * Retrieves a resumable upload URL you can use to upload your main message backup file or an arbitrary media file to cloud storage. */ - fun getBackupResumableUploadUrl(uploadForm: AttachmentUploadForm): NetworkResult { - return NetworkResult.fromFetch { - pushServiceSocket.getResumableUploadUrl(uploadForm) - } - } + fun getBackupResumableUploadUrl(uploadForm: AttachmentUploadForm): NetworkResult /** * Uploads your main backup file to cloud storage. */ - fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult { - return NetworkResult.fromFetch { - pushServiceSocket.uploadBackupFile(uploadForm, resumableUploadUrl, data, dataLength) - } - } + fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult /** * Retrieves an [AttachmentUploadForm] that can be used to upload pre-existing media to the archive. * After uploading, the media still needs to be copied via [archiveAttachmentMedia]. */ - fun getMediaUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation()) - } - } + fun getMediaUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult - fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult { - return NetworkResult.fromFetch { - if (secretKey == null) { - pushServiceSocket.getResumableUploadSpec(uploadForm) - } else { - pushServiceSocket.getResumableUploadSpecWithKey(uploadForm, secretKey) - } - } - } + fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult /** * 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, serviceCredential: ArchiveServiceCredential): NetworkResult> { - return NetworkResult.fromFetch { - val mediaObjects: MutableList = ArrayList() - - var cursor: String? = null - do { - val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, serviceCredential, 512, cursor).successOrThrow() - mediaObjects += response.storedMediaObjects - cursor = response.cursor - } while (cursor != null) - - mediaObjects - } - } - + fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult> /** * Retrieves a page of media items in the user's archive. * @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, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - - pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) - } - } + fun getArchiveMediaItemsPage(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult /** * Copy and re-encrypt media from the attachments cdn into the backup cdn. @@ -213,14 +107,7 @@ class ArchiveApi( backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, item: ArchiveMediaRequest - ): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - - pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item) - } - } + ): NetworkResult /** * Copy and re-encrypt media from the attachments cdn into the backup cdn. @@ -229,16 +116,7 @@ class ArchiveApi( backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, items: List - ): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - - val request = BatchArchiveMediaRequest(items = items) - - pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), request) - } - } + ): NetworkResult /** * Delete media from the backup cdn. @@ -247,49 +125,7 @@ class ArchiveApi( backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, mediaToDelete: List - ): NetworkResult { - return NetworkResult.fromFetch { - val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) - val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete) - - pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request) - } - } - - fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential { - val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential) - val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) - - return backupRequestContext.receiveResponse( - backupAuthResponse, - Instant.ofEpochSecond(serviceCredential.redemptionTime), - backupServerPublicParams - ) - } - - private class CredentialPresentationData( - val privateKey: ECPrivateKey, - val presentation: ByteArray, - val signedPresentation: ByteArray - ) { - val publicKey: ECPublicKey = privateKey.publicKey() - - companion object { - fun from(backupKey: BackupKey, credential: BackupAuthCredential, backupServerPublicParams: GenericServerPublicParams): CredentialPresentationData { - val privateKey: ECPrivateKey = Curve.decodePrivatePoint(backupKey.value) - val presentation: ByteArray = credential.present(backupServerPublicParams).serialize() - val signedPresentation: ByteArray = privateKey.calculateSignature(presentation) - - return CredentialPresentationData(privateKey, presentation, signedPresentation) - } - } + ): NetworkResult - fun toArchiveCredentialPresentation(): ArchiveCredentialPresentation { - return ArchiveCredentialPresentation( - presentation = presentation, - signedPresentation = signedPresentation - ) - } - } + fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/LocalArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/LocalArchiveApi.kt new file mode 100644 index 0000000000..23d9db58f3 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/LocalArchiveApi.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import org.signal.libsignal.zkgroup.backups.BackupAuthCredential +import org.signal.libsignal.zkgroup.backups.BackupLevel +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.internal.push.AttachmentUploadForm +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec +import java.io.InputStream + +/** + * Class to interact with various archive-related endpoints. + * Why is it called archive instead of backup? Because SVR took the "backup" endpoint namespace first :) + */ +class LocalArchiveApi: ArchiveApi { + private val LOCAL_CDN = -1 + + /** + * Retrieves a set of credentials one can use to authorize other requests. + * + * You'll receive a set of credentials spanning 7 days. Cache them and store them for later use. + * It's important that (at least in the common case) you do not request credentials on-the-fly. + * Instead, request them in advance on a regular schedule. This is because the purpose of these + * 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. + */ + override fun getServiceCredentials(currentTime: Long): NetworkResult { + // Not used for local archive + return NetworkResult.Success(ArchiveServiceCredentialsResponse(emptyArray())) + } + + override fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + // Not used for local archive + return NetworkResult.Success(GetArchiveCdnCredentialsResponse(emptyMap())) + } + + /** + * Ensures that you reserve a backupId 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. + */ + override fun triggerBackupIdReservation(backupKey: BackupKey): NetworkResult { + // Not used for local archive + return NetworkResult.Success(Unit) + } + + /** + * Sets a public key on the service derived from your [BackupKey]. 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. + */ + override fun setPublicKey(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + // Not used for local archive + return NetworkResult.Success(Unit) + } + + /** + * Fetches an upload form you can use to upload your main message backup file to cloud storage. + */ + override fun getMessageBackupUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + // This form is intended to be used by getBackupResumableUploadUrl + return NetworkResult.Success( + AttachmentUploadForm(LOCAL_CDN,"", emptyMap(),"") + ) + } + + /** + * Fetches metadata about your current backup. + * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a + * backup yet. + */ + override fun getBackupInfo(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + //TODO + // If the backup is not initialized => 404 + // Else => Success + // cdn must be < 0 to make clear it is local + } + + /** + * Lists the media objects in the backup + */ + override fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { + //TODO list local media + } + + /** + * Retrieves a resumable upload URL you can use to upload your main message backup file or an arbitrary media file to cloud storage. + */ + override fun getBackupResumableUploadUrl(uploadForm: AttachmentUploadForm): NetworkResult { + // This URL is intended to be used by uploadBackupFile + return NetworkResult.Success("") + } + + /** + * Uploads your main backup file to cloud storage. + */ + override fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult { + //TODO write files locally + } + + /** + * Retrieves an [AttachmentUploadForm] that can be used to upload pre-existing media to the archive. + * After uploading, the media still needs to be copied via [archiveAttachmentMedia]. + */ + override fun getMediaUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + // This form is intended to be used by getResumableUploadSpec + return NetworkResult.Success(AttachmentUploadForm(LOCAL_CDN, "", emptyMap(), "")) + } + + override fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult { + // org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaUploadSpec + // 1. org.thoughtcrime.securesms.jobs.ArchiveAttachmentBackfillJob.fetchResumableUploadSpec + // org.thoughtcrime.securesms.jobs.ArchiveAttachmentBackfillJob.run + // 2. org.thoughtcrime.securesms.jobs.ArchiveThumbnailUploadJob.run + // Both are used to create stream used by AppDependencies.signalServiceMessageSender.uploadAttachment + } + + /** + * 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. + */ + override fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult> { + /* + val mediaObjects: MutableList = ArrayList() + + var cursor: String? = null + do { + val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, serviceCredential, 512, cursor).successOrThrow() + mediaObjects += response.storedMediaObjects + cursor = response.cursor + } while (cursor != null) + + mediaObjects + */ + } + + /** + * Retrieves a page of media items in the user's archive. + * @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. + */ + override fun getArchiveMediaItemsPage(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { + // org.thoughtcrime.securesms.backup.v2.BackupRepository.listRemoteMediaObjects + // org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob.onRun + } + + /** + * Copy and re-encrypt media from the attachments cdn into the backup cdn. + * + * Possible errors: + * 400: Bad arguments, or made on an authenticated channel + * 401: Invalid presentation or signature + * 403: Insufficient permissions + * 413: No media space remaining + * 429: Rate-limited + */ + override fun archiveAttachmentMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + item: ArchiveMediaRequest + ): NetworkResult { + // This is where attachments are stored + } + + /** + * Copy and re-encrypt media from the attachments cdn into the backup cdn. + */ + override fun archiveAttachmentMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + items: List + ): NetworkResult { + // This is where attachments are stored in batch + } + + /** + * Delete media from the backup cdn. + */ + override fun deleteArchivedMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + mediaToDelete: List + ): NetworkResult { + + } + + override fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential { + // zkCredential.backupLevel must be BackupLevel.MEDIA + // It is only used once in org.thoughtcrime.securesms.backup.v2.BackupRepository + return BackupAuthCredential(byteArrayOf(BackupLevel.MEDIA.ordinal.toByte())) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/RemoteArchiveApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/RemoteArchiveApi.kt new file mode 100644 index 0000000000..0bbbe5bc0b --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/RemoteArchiveApi.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import org.signal.libsignal.protocol.ecc.Curve +import org.signal.libsignal.protocol.ecc.ECPrivateKey +import org.signal.libsignal.protocol.ecc.ECPublicKey +import org.signal.libsignal.zkgroup.GenericServerPublicParams +import org.signal.libsignal.zkgroup.backups.BackupAuthCredential +import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequestContext +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.push.ServiceId.ACI +import org.whispersystems.signalservice.internal.push.AttachmentUploadForm +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec +import java.io.InputStream +import java.time.Instant + +// MOLLY: This is ArchiveApi from upstream + +/** + * Class to interact with various archive-related endpoints. + * Why is it called archive instead of backup? Because SVR took the "backup" endpoint namespace first :) + */ +class RemoteArchiveApi( + private val pushServiceSocket: PushServiceSocket, + private val backupServerPublicParams: GenericServerPublicParams, + private val aci: ACI +): ArchiveApi { + companion object { + @JvmStatic + fun create(pushServiceSocket: PushServiceSocket, backupServerPublicParams: ByteArray, aci: ACI): RemoteArchiveApi { + return RemoteArchiveApi( + pushServiceSocket, + GenericServerPublicParams(backupServerPublicParams), + aci + ) + } + } + + /** + * Retrieves a set of credentials one can use to authorize other requests. + * + * You'll receive a set of credentials spanning 7 days. Cache them and store them for later use. + * It's important that (at least in the common case) you do not request credentials on-the-fly. + * Instead, request them in advance on a regular schedule. This is because the purpose of these + * 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. + */ + override fun getServiceCredentials(currentTime: Long): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getArchiveCredentials(currentTime) + } + } + + override fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + + pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation()) + } + } + + /** + * Ensures that you reserve a backupId 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. + */ + override fun triggerBackupIdReservation(backupKey: BackupKey): NetworkResult { + return NetworkResult.fromFetch { + val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) + pushServiceSocket.setArchiveBackupId(backupRequestContext.request) + } + } + + /** + * Sets a public key on the service derived from your [BackupKey]. 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. + */ + override fun setPublicKey(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + pushServiceSocket.setArchivePublicKey(presentationData.publicKey, presentationData.toArchiveCredentialPresentation()) + } + } + + /** + * Fetches an upload form you can use to upload your main message backup file to cloud storage. + */ + override fun getMessageBackupUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation()) + } + } + + /** + * Fetches metadata about your current backup. + * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a + * backup yet. + */ + override fun getBackupInfo(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation()) + } + } + + /** + * Lists the media objects in the backup + */ + override fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) + } + } + + /** + * Retrieves a resumable upload URL you can use to upload your main message backup file or an arbitrary media file to cloud storage. + */ + override fun getBackupResumableUploadUrl(uploadForm: AttachmentUploadForm): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getResumableUploadUrl(uploadForm) + } + } + + /** + * Uploads your main backup file to cloud storage. + */ + override fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.uploadBackupFile(uploadForm, resumableUploadUrl, data, dataLength) + } + } + + /** + * Retrieves an [AttachmentUploadForm] that can be used to upload pre-existing media to the archive. + * After uploading, the media still needs to be copied via [archiveAttachmentMedia]. + */ + override fun getMediaUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation()) + } + } + + override fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult { + return NetworkResult.fromFetch { + if (secretKey == null) { + pushServiceSocket.getResumableUploadSpec(uploadForm) + } else { + pushServiceSocket.getResumableUploadSpecWithKey(uploadForm, secretKey) + } + } + } + + /** + * 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. + */ + override fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult> { + return NetworkResult.fromFetch { + val mediaObjects: MutableList = ArrayList() + + var cursor: String? = null + do { + val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, serviceCredential, 512, cursor).successOrThrow() + mediaObjects += response.storedMediaObjects + cursor = response.cursor + } while (cursor != null) + + mediaObjects + } + } + + /** + * Retrieves a page of media items in the user's archive. + * @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. + */ + override fun getArchiveMediaItemsPage(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + + pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) + } + } + + /** + * Copy and re-encrypt media from the attachments cdn into the backup cdn. + * + * Possible errors: + * 400: Bad arguments, or made on an authenticated channel + * 401: Invalid presentation or signature + * 403: Insufficient permissions + * 413: No media space remaining + * 429: Rate-limited + */ + override fun archiveAttachmentMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + item: ArchiveMediaRequest + ): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + + pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item) + } + } + + /** + * Copy and re-encrypt media from the attachments cdn into the backup cdn. + */ + override fun archiveAttachmentMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + items: List + ): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + + val request = BatchArchiveMediaRequest(items = items) + + pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), request) + } + } + + /** + * Delete media from the backup cdn. + */ + override fun deleteArchivedMedia( + backupKey: BackupKey, + serviceCredential: ArchiveServiceCredential, + mediaToDelete: List + ): NetworkResult { + return NetworkResult.fromFetch { + val zkCredential = getZkCredential(backupKey, serviceCredential) + val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete) + + pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request) + } + } + + override fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential { + val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential) + val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) + + return backupRequestContext.receiveResponse( + backupAuthResponse, + Instant.ofEpochSecond(serviceCredential.redemptionTime), + backupServerPublicParams + ) + } + + private class CredentialPresentationData( + val privateKey: ECPrivateKey, + val presentation: ByteArray, + val signedPresentation: ByteArray + ) { + val publicKey: ECPublicKey = privateKey.publicKey() + + companion object { + fun from(backupKey: BackupKey, credential: BackupAuthCredential, backupServerPublicParams: GenericServerPublicParams): CredentialPresentationData { + val privateKey: ECPrivateKey = Curve.decodePrivatePoint(backupKey.value) + val presentation: ByteArray = credential.present(backupServerPublicParams).serialize() + val signedPresentation: ByteArray = privateKey.calculateSignature(presentation) + + return CredentialPresentationData(privateKey, presentation, signedPresentation) + } + } + + fun toArchiveCredentialPresentation(): ArchiveCredentialPresentation { + return ArchiveCredentialPresentation( + presentation = presentation, + signedPresentation = signedPresentation + ) + } + } +}