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 f4aeb2ccb3..781d68097b 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 @@ -392,7 +392,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getArchiveMediaItemsPage(backupKey, credential, limit, cursor) + api.getArchiveMediaItemsPage(backupKey, SignalStore.account.requireAci(), credential, limit, cursor) } } @@ -402,7 +402,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getBackupInfo(backupKey, credential) + api.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) .map { it.usedSpace } } } @@ -431,12 +431,12 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getBackupInfo(backupKey, credential) + api.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) .map { it to credential } } .then { pair -> val (info, credential) = pair - api.debugGetUploadedMediaItemMetadata(backupKey, credential) + api.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential) .also { Log.i(TAG, "MediaItemMetadataResult: $it") } .map { mediaObjects -> BackupMetadata( @@ -458,7 +458,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getMessageBackupUploadForm(backupKey, credential) + api.getMessageBackupUploadForm(backupKey, SignalStore.account.requireAci(), credential) .also { Log.i(TAG, "UploadFormResult: $it") } } .then { form -> @@ -480,7 +480,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getBackupInfo(backupKey, credential) + api.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) } .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .map { pair -> @@ -496,7 +496,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getBackupInfo(backupKey, credential) + api.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential) } .then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } } .then { pair -> @@ -517,7 +517,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.debugGetUploadedMediaItemMetadata(backupKey, credential) + api.debugGetUploadedMediaItemMetadata(backupKey, SignalStore.account.requireAci(), credential) } } @@ -530,7 +530,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getMediaUploadForm(backupKey, credential) + api.getMediaUploadForm(backupKey, SignalStore.account.requireAci(), credential) } .then { form -> api.getResumableUploadSpec(form, secretKey) @@ -546,6 +546,7 @@ object BackupRepository { .then { credential -> api.archiveAttachmentMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, item = request ) @@ -563,6 +564,7 @@ object BackupRepository { api .archiveAttachmentMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, item = request ) @@ -596,6 +598,7 @@ object BackupRepository { api .archiveAttachmentMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, items = requests ) @@ -637,6 +640,7 @@ object BackupRepository { .then { credential -> api.deleteArchivedMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, mediaToDelete = mediaToDelete ) @@ -668,6 +672,7 @@ object BackupRepository { .then { credential -> api.deleteArchivedMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, mediaToDelete = mediaToDelete ) @@ -697,6 +702,7 @@ object BackupRepository { .then { credential -> api.deleteArchivedMedia( backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential, mediaToDelete = mediaToDelete ) @@ -726,6 +732,7 @@ object BackupRepository { api.getCdnReadCredentials( cdnNumber = cdnNumber, backupKey = backupKey, + aci = SignalStore.account.requireAci(), serviceCredential = credential ) } @@ -781,7 +788,7 @@ object BackupRepository { return initBackupAndFetchAuth(backupKey) .then { credential -> - api.getBackupInfo(backupKey, credential).map { + api.getBackupInfo(backupKey, SignalStore.account.requireAci(), credential).map { SignalStore.backup.usedBackupMediaSpace = it.usedSpace ?: 0L BackupDirectories(it.backupDir!!, it.mediaDir!!) } @@ -883,7 +890,7 @@ object BackupRepository { return api .triggerBackupIdReservation(backupKey) .then { getAuthCredential() } - .then { credential -> api.setPublicKey(backupKey, credential).map { credential } } + .then { credential -> api.setPublicKey(backupKey, SignalStore.account.requireAci(), credential).map { credential } } .runIfSuccessful { SignalStore.backup.backupsInitialized = true } .runOnStatusCodeError(resetInitializedStateErrorAction) } 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 38d418a736..dfba28c44b 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 @@ -25,7 +25,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator @@ -257,12 +259,15 @@ fun Screen( onTriggerBackupJobClicked: () -> Unit = {}, onRestoreFromRemoteClicked: () -> Unit = {} ) { + val scrollState = rememberScrollState() + Surface { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() + .verticalScroll(scrollState) .padding(16.dp) ) { Row( 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..4e9ec6b786 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,7 +5,6 @@ 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 @@ -58,10 +57,10 @@ class ArchiveApi( } } - fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveCdnReadCredentials(cdnNumber, presentationData.toArchiveCredentialPresentation()) } @@ -83,10 +82,10 @@ class ArchiveApi( * 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 { + fun setPublicKey(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.setArchivePublicKey(presentationData.publicKey, presentationData.toArchiveCredentialPresentation()) } } @@ -94,10 +93,10 @@ class ArchiveApi( /** * Fetches an upload form you can use to upload your main message backup file to cloud storage. */ - fun getMessageBackupUploadForm(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getMessageBackupUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMessageBackupUploadForm(presentationData.toArchiveCredentialPresentation()) } } @@ -107,10 +106,10 @@ class ArchiveApi( * Will return a [NetworkResult.StatusCodeError] with status code 404 if you haven't uploaded a * backup yet. */ - fun getBackupInfo(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult { + fun getBackupInfo(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveBackupInfo(presentationData.toArchiveCredentialPresentation()) } } @@ -118,10 +117,10 @@ class ArchiveApi( /** * Lists the media objects in the backup */ - fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { + fun listMediaObjects(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) } } @@ -148,10 +147,10 @@ class ArchiveApi( * 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 { + fun getMediaUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaUploadForm(presentationData.toArchiveCredentialPresentation()) } } @@ -170,13 +169,13 @@ class ArchiveApi( * 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> { + fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult> { return NetworkResult.fromFetch { val mediaObjects: MutableList = ArrayList() var cursor: String? = null do { - val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, serviceCredential, 512, cursor).successOrThrow() + val response: ArchiveGetMediaItemsResponse = getArchiveMediaItemsPage(backupKey, aci, serviceCredential, 512, cursor).successOrThrow() mediaObjects += response.storedMediaObjects cursor = response.cursor } while (cursor != null) @@ -190,10 +189,10 @@ class ArchiveApi( * @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 { + fun getArchiveMediaItemsPage(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.getArchiveMediaItemsPage(presentationData.toArchiveCredentialPresentation(), limit, cursor) } @@ -211,12 +210,13 @@ class ArchiveApi( */ fun archiveAttachmentMedia( backupKey: BackupKey, + aci: ACI, serviceCredential: ArchiveServiceCredential, item: ArchiveMediaRequest ): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item) } @@ -227,12 +227,13 @@ class ArchiveApi( */ fun archiveAttachmentMedia( backupKey: BackupKey, + aci: ACI, serviceCredential: ArchiveServiceCredential, items: List ): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) val request = BatchArchiveMediaRequest(items = items) @@ -245,12 +246,13 @@ class ArchiveApi( */ fun deleteArchivedMedia( backupKey: BackupKey, + aci: ACI, serviceCredential: ArchiveServiceCredential, mediaToDelete: List ): NetworkResult { return NetworkResult.fromFetch { val zkCredential = getZkCredential(backupKey, serviceCredential) - val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams) + val presentationData = CredentialPresentationData.from(backupKey, aci, zkCredential, backupServerPublicParams) val request = DeleteArchivedMediaRequest(mediaToDelete = mediaToDelete) pushServiceSocket.deleteArchivedMedia(presentationData.toArchiveCredentialPresentation(), request) @@ -276,8 +278,8 @@ class ArchiveApi( val publicKey: ECPublicKey = privateKey.publicKey() companion object { - fun from(backupKey: BackupKey, credential: BackupAuthCredential, backupServerPublicParams: GenericServerPublicParams): CredentialPresentationData { - val privateKey: ECPrivateKey = Curve.decodePrivatePoint(backupKey.value) + fun from(backupKey: BackupKey, aci: ACI, credential: BackupAuthCredential, backupServerPublicParams: GenericServerPublicParams): CredentialPresentationData { + val privateKey: ECPrivateKey = backupKey.deriveAnonymousCredentialPrivateKey(aci) val presentation: ByteArray = credential.present(backupServerPublicParams).serialize() val signedPresentation: ByteArray = privateKey.calculateSignature(presentation) 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 abca72a70c..d9ba9f4b29 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 @@ -5,6 +5,8 @@ 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 @@ -16,12 +18,18 @@ class BackupKey(val value: ByteArray) { require(value.size == 32) { "Backup key must be 32 bytes!" } } + /** + * 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) @@ -34,6 +42,14 @@ class BackupKey(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(), "Media ID".toByteArray(), 15)) }