Fix network interaction in backup service.
This commit is contained in:
parent
d9586e8d00
commit
ea215ef488
4 changed files with 63 additions and 33 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue