Skip to content
This repository has been archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
Prepare local backup v2
Browse files Browse the repository at this point in the history
  • Loading branch information
p1gp1g committed Sep 8, 2024
1 parent 0dfe2b2 commit 54e6179
Show file tree
Hide file tree
Showing 5 changed files with 523 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<ArchiveServiceCredentialsResponse> {
return NetworkResult.fromFetch {
pushServiceSocket.getArchiveCredentials(currentTime)
}
}
fun getServiceCredentials(currentTime: Long): NetworkResult<ArchiveServiceCredentialsResponse>

fun getCdnReadCredentials(cdnNumber: Int, backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<GetArchiveCdnCredentialsResponse> {
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<GetArchiveCdnCredentialsResponse>

/**
* 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<Unit> {
return NetworkResult.fromFetch {
val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid)
pushServiceSocket.setArchiveBackupId(backupRequestContext.request)
}
}
fun triggerBackupIdReservation(backupKey: BackupKey): NetworkResult<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.
*/
fun setPublicKey(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): NetworkResult<Unit> {
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<Unit>

/**
* 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> {
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<AttachmentUploadForm>

/**
* 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<ArchiveGetBackupInfoResponse> {
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<ArchiveGetBackupInfoResponse>

/**
* Lists the media objects in the backup
*/
fun listMediaObjects(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential, limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
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<ArchiveGetMediaItemsResponse>

/**
* 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<String> {
return NetworkResult.fromFetch {
pushServiceSocket.getResumableUploadUrl(uploadForm)
}
}
fun getBackupResumableUploadUrl(uploadForm: AttachmentUploadForm): NetworkResult<String>

/**
* Uploads your main backup file to cloud storage.
*/
fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult<Unit> {
return NetworkResult.fromFetch {
pushServiceSocket.uploadBackupFile(uploadForm, resumableUploadUrl, data, dataLength)
}
}
fun uploadBackupFile(uploadForm: AttachmentUploadForm, resumableUploadUrl: String, data: InputStream, dataLength: Long): NetworkResult<Unit>

/**
* 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> {
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<AttachmentUploadForm>

fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult<ResumableUploadSpec> {
return NetworkResult.fromFetch {
if (secretKey == null) {
pushServiceSocket.getResumableUploadSpec(uploadForm)
} else {
pushServiceSocket.getResumableUploadSpecWithKey(uploadForm, secretKey)
}
}
}
fun getResumableUploadSpec(uploadForm: AttachmentUploadForm, secretKey: ByteArray?): NetworkResult<ResumableUploadSpec>

/**
* 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>> {
return NetworkResult.fromFetch {
val mediaObjects: MutableList<StoredMediaObject> = 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<List<StoredMediaObject>>
/**
* 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<ArchiveGetMediaItemsResponse> {
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<ArchiveGetMediaItemsResponse>

/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
Expand All @@ -213,14 +107,7 @@ class ArchiveApi(
backupKey: BackupKey,
serviceCredential: ArchiveServiceCredential,
item: ArchiveMediaRequest
): NetworkResult<ArchiveMediaResponse> {
return NetworkResult.fromFetch {
val zkCredential = getZkCredential(backupKey, serviceCredential)
val presentationData = CredentialPresentationData.from(backupKey, zkCredential, backupServerPublicParams)

pushServiceSocket.archiveAttachmentMedia(presentationData.toArchiveCredentialPresentation(), item)
}
}
): NetworkResult<ArchiveMediaResponse>

/**
* Copy and re-encrypt media from the attachments cdn into the backup cdn.
Expand All @@ -229,16 +116,7 @@ class ArchiveApi(
backupKey: BackupKey,
serviceCredential: ArchiveServiceCredential,
items: List<ArchiveMediaRequest>
): NetworkResult<BatchArchiveMediaResponse> {
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<BatchArchiveMediaResponse>

/**
* Delete media from the backup cdn.
Expand All @@ -247,49 +125,7 @@ class ArchiveApi(
backupKey: BackupKey,
serviceCredential: ArchiveServiceCredential,
mediaToDelete: List<DeleteArchivedMediaRequest.ArchivedMediaObject>
): NetworkResult<Unit> {
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<Unit>

fun toArchiveCredentialPresentation(): ArchiveCredentialPresentation {
return ArchiveCredentialPresentation(
presentation = presentation,
signedPresentation = signedPresentation
)
}
}
fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential
}
Loading

0 comments on commit 54e6179

Please sign in to comment.