Fix network interaction in backup service.

This commit is contained in:
Greyson Parrelli 2024-07-31 12:34:34 -04:00 committed by mtang-signal
parent d9586e8d00
commit ea215ef488
4 changed files with 63 additions and 33 deletions

View file

@ -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)
}

View file

@ -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(

View file

@ -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<GetArchiveCdnCredentialsResponse> {
fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<GetArchiveCdnCredentialsResponse> {
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<Unit> {
fun setPublicKey(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<Unit> {
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<AttachmentUploadForm> {
fun getMessageBackupUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
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<ArchiveGetBackupInfoResponse> {
fun getBackupInfo(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<ArchiveGetBackupInfoResponse> {
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<ArchiveGetMediaItemsResponse> {
fun listMediaObjects(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
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<AttachmentUploadForm> {
fun getMediaUploadForm(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<AttachmentUploadForm> {
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<List<StoredMediaObject>> {
fun debugGetUploadedMediaItemMetadata(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential): NetworkResult<List<StoredMediaObject>> {
return NetworkResult.fromFetch {
val mediaObjects: MutableList<StoredMediaObject> = 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<ArchiveGetMediaItemsResponse> {
fun getArchiveMediaItemsPage(backupKey: BackupKey, aci: ACI, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String?): NetworkResult<ArchiveGetMediaItemsResponse> {
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<ArchiveMediaResponse> {
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<ArchiveMediaRequest>
): NetworkResult<BatchArchiveMediaResponse> {
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<DeleteArchivedMediaRequest.ArchivedMediaObject>
): NetworkResult<Unit> {
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)

View file

@ -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))
}