From 6230a7553dc273b6217a02d7bc551fc28efe8569 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 7 Dec 2023 16:11:57 -0500 Subject: [PATCH] Add some initial backupV2 network infrastructure. --- app/build.gradle.kts | 2 + ...umentationApplicationDependencyProvider.kt | 3 +- .../securesms/backup/v2/BackupRepository.kt | 52 +++++++ .../InternalBackupPlaygroundFragment.kt | 12 +- .../InternalBackupPlaygroundViewModel.kt | 7 + .../securesms/keyvalue/BackupValues.kt | 76 ++++++++++ .../securesms/keyvalue/SignalStore.java | 6 + .../push/SignalServiceNetworkAccess.kt | 15 +- .../signalservice/api/NetworkResult.kt | 93 ++++++++++++ .../api/SignalServiceAccountManager.java | 5 + .../signalservice/api/archive/ArchiveApi.kt | 139 ++++++++++++++++++ .../archive/ArchiveCredentialPresentation.kt | 14 ++ .../archive/ArchiveGetBackupInfoResponse.kt | 22 +++ .../ArchiveMessageBackupUploadFormResponse.kt | 22 +++ .../api/archive/ArchiveServiceCredential.kt | 15 ++ .../ArchiveServiceCredentialsResponse.kt | 17 +++ .../api/archive/ArchiveSetBackupIdRequest.kt | 29 ++++ .../api/archive/ArchiveSetPublicKeyRequest.kt | 29 ++++ .../SignalServiceConfiguration.kt | 3 +- .../internal/push/PushServiceSocket.java | 59 +++++++- 20 files changed, 609 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveCredentialPresentation.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveGetBackupInfoResponse.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMessageBackupUploadFormResponse.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredential.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetPublicKeyRequest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9a1c613a06..4c8ca5fe01 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -207,6 +207,7 @@ android { buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"") buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\"") buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"") + buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"") buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }") buildConfigField("int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode") buildConfigField("String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"") @@ -387,6 +388,7 @@ android { buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"") buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\"") buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"") + buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"") buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"") buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"") buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"") diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt index 24bd1b35dd..a883661547 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt @@ -80,7 +80,8 @@ class InstrumentationApplicationDependencyProvider(application: Application, def dns = Optional.of(SignalServiceNetworkAccess.DNS), signalProxy = Optional.empty(), zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS), - genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS) + genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS), + backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS) ) serviceNetworkAccessMock = mock { 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 12694f2325..0d33852be3 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 @@ -21,12 +21,16 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.PNI import java.io.ByteArrayOutputStream import java.io.InputStream +import kotlin.time.Duration.Companion.milliseconds object BackupRepository { @@ -149,6 +153,54 @@ object BackupRepository { Log.d(TAG, "import() ${eventTimer.stop().summary}") } + /** + * A simple test method that just hits various network endpoints. Only useful for the playground. + */ + fun testNetworkInteractions() { + val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi + val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() + + // Just running some sample requests + api + .triggerBackupIdReservation(backupKey) + .then { getAuthCredential() } + .then { credential -> + api.setPublicKey(backupKey, credential) + .also { Log.i(TAG, "PublicKeyResult: $it") } + .map { credential } + } + .then { credential -> + api.getBackupInfo(backupKey, credential) + .also { Log.i(TAG, "BackupInfoResult: $it") } + .map { credential } + } + .then { credential -> + api.getMessageBackupUploadForm(backupKey, credential) + .also { Log.i(TAG, "UploadFormResult: $it") } + }.also { Log.i(TAG, "OverallResponse: $it") } + } + + /** + * Retrieves an auth credential, preferring a cached value if available. + */ + private fun getAuthCredential(): NetworkResult { + val currentTime = System.currentTimeMillis() + + val credential = SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds) + + if (credential != null) { + return NetworkResult.Success(credential) + } + + Log.w(TAG, "No credentials found for today, need to fetch new ones! This shouldn't happen under normal circumstances. We should ensure the routine fetch is running properly.") + + return ApplicationDependencies.getSignalServiceAccountManager().archiveApi.getServiceCredentials(currentTime).map { result -> + SignalStore.backup().addCredentials(result.credentials.toList()) + SignalStore.backup().clearCredentialsOlderThan(currentTime) + SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)!! + } + } + data class SelfData( val aci: ACI, val pni: PNI, 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 3d35f92578..4dd7692d4d 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 @@ -97,7 +97,8 @@ class InternalBackupPlaygroundFragment : ComposeFragment() { } exportFileLauncher.launch(intent) - } + }, + onTestNetworkClicked = { viewModel.testNetworkInteractions() } ) } @@ -113,7 +114,8 @@ fun Screen( onImportMemoryClicked: () -> Unit = {}, onImportFileClicked: () -> Unit = {}, onPlaintextClicked: () -> Unit = {}, - onSaveToDiskClicked: () -> Unit = {} + onSaveToDiskClicked: () -> Unit = {}, + onTestNetworkClicked: () -> Unit = {} ) { Surface { Column( @@ -142,6 +144,12 @@ fun Screen( ) { Text("Export") } + Buttons.LargePrimary( + onClick = onTestNetworkClicked, + enabled = state.backupState == BackupState.EXPORT_DONE + ) { + Text("Test network") + } Buttons.LargeTonal( onClick = onImportMemoryClicked, enabled = state.backupState == BackupState.EXPORT_DONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index fe91923e06..b3956e47ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -80,6 +80,13 @@ class InternalBackupPlaygroundViewModel : ViewModel() { _state.value = _state.value.copy(plaintext = !_state.value.plaintext) } + fun testNetworkInteractions() { + disposables += Single + .fromCallable { BackupRepository.testNetworkInteractions() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + override fun onCleared() { disposables.clear() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt new file mode 100644 index 0000000000..fe5332e7d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.keyvalue + +import com.fasterxml.jackson.annotation.JsonProperty +import org.signal.core.util.logging.Log +import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential +import org.whispersystems.signalservice.internal.util.JsonUtil +import java.io.IOException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days + +internal class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { + companion object { + val TAG = Log.tag(BackupValues::class.java) + val KEY_CREDENTIALS = "backup.credentials" + } + + override fun onFirstEverAppLaunch() = Unit + override fun getKeysToIncludeInBackup(): List = emptyList() + + /** + * Retrieves the stored credentials, mapped by the day they're valid. The day is represented as + * the unix time (in seconds) of the start of the day. Wrapped in a [ArchiveServiceCredentials] + * type to make it easier to use. See [ArchiveServiceCredentials.getForCurrentTime]. + */ + val credentialsByDay: ArchiveServiceCredentials + get() { + val serialized = store.getString(KEY_CREDENTIALS, null) ?: return ArchiveServiceCredentials() + + return try { + val map = JsonUtil.fromJson(serialized, SerializedCredentials::class.java).credentialsByDay + ArchiveServiceCredentials(map) + } catch (e: IOException) { + Log.w(TAG, "Invalid JSON! Clearing.", e) + putString(KEY_CREDENTIALS, null) + ArchiveServiceCredentials() + } + } + + /** + * Adds the given credentials to the existing list of stored credentials. + */ + fun addCredentials(credentials: List) { + val current: MutableMap = credentialsByDay.toMutableMap() + current.putAll(credentials.associateBy { it.redemptionTime }) + putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(current))) + } + + /** + * Trims out any credentials that are for days older than the given timestamp. + */ + fun clearCredentialsOlderThan(startOfDayInSeconds: Long) { + val current: MutableMap = credentialsByDay.toMutableMap() + val updated = current.filterKeys { it < startOfDayInSeconds } + putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(updated))) + } + + class SerializedCredentials( + @JsonProperty + val credentialsByDay: Map + ) + + /** + * A [Map] wrapper that makes it easier to get the credential for the current time. + */ + class ArchiveServiceCredentials(map: Map) : Map by map { + constructor() : this(mapOf()) + + /** + * Retrieves a credential that is valid for the current time, otherwise null. + */ + fun getForCurrentTime(currentTime: Duration): ArchiveServiceCredential? { + val startOfDayInSeconds: Long = currentTime.inWholeDays.days.inWholeSeconds + return this[startOfDayInSeconds] + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index bbb0e8ee04..f30c715b64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -44,6 +44,7 @@ public final class SignalStore { private final ReleaseChannelValues releaseChannelValues; private final StoryValues storyValues; private final ApkUpdateValues apkUpdate; + private final BackupValues backupValues; private final PlainTextSharedPrefsDataStore plainTextValues; @@ -89,6 +90,7 @@ public final class SignalStore { this.releaseChannelValues = new ReleaseChannelValues(store); this.storyValues = new StoryValues(store); this.apkUpdate = new ApkUpdateValues(store); + this.backupValues = new BackupValues(store); this.plainTextValues = new PlainTextSharedPrefsDataStore(ApplicationDependencies.getApplication()); } @@ -270,6 +272,10 @@ public final class SignalStore { return getInstance().apkUpdate; } + public static @NonNull BackupValues backup() { + return getInstance().backupValues; + } + public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AciAuthorizationCache() { return GroupsV2AuthorizationSignalStoreCache.createAciCache(getStore()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index acc1c0f110..6eafb03261 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -158,6 +158,12 @@ open class SignalServiceNetworkAccess(context: Context) { throw AssertionError(e) } + private val backupServerPublicParams: ByteArray = try { + Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS) + } catch (e: IOException) { + throw AssertionError(e) + } + private val baseGHostConfigs: List = listOf( HostConfig("https://www.google.com", G_HOST, GMAIL_CONNECTION_SPEC), HostConfig("https://android.clients.google.com", G_HOST, PLAY_CONNECTION_SPEC), @@ -182,7 +188,8 @@ open class SignalServiceNetworkAccess(context: Context) { dns = Optional.of(DNS), signalProxy = Optional.empty(), zkGroupServerPublicParams = zkGroupServerPublicParams, - genericServerPublicParams = genericServerPublicParams + genericServerPublicParams = genericServerPublicParams, + backupServerPublicParams = backupServerPublicParams ) private val censorshipConfiguration: Map = mapOf( @@ -234,7 +241,8 @@ open class SignalServiceNetworkAccess(context: Context) { dns = Optional.of(DNS), signalProxy = if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(), zkGroupServerPublicParams = zkGroupServerPublicParams, - genericServerPublicParams = genericServerPublicParams + genericServerPublicParams = genericServerPublicParams, + backupServerPublicParams = backupServerPublicParams ) open fun getConfiguration(): SignalServiceConfiguration { @@ -303,7 +311,8 @@ open class SignalServiceNetworkAccess(context: Context) { dns = Optional.of(DNS), signalProxy = Optional.empty(), zkGroupServerPublicParams = zkGroupServerPublicParams, - genericServerPublicParams = genericServerPublicParams + genericServerPublicParams = genericServerPublicParams, + backupServerPublicParams = backupServerPublicParams ) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt new file mode 100644 index 0000000000..12981b17df --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api + +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException +import java.io.IOException + +/** + * A helper class that wraps the result of a network request, turning common exceptions + * into sealed classes, with optional request chaining. + * + * This was designed to be a middle ground between the heavy reliance on specific exceptions + * in old network code (which doesn't translate well to kotlin not having checked exceptions) + * and plain rx, which still doesn't free you from having to catch exceptions and translate + * things to sealed classes yourself. + * + * If you have a very complicated network request with lots of different possible response types + * based on specific errors, this isn't for you. You're likely better off writing your own + * sealed class. However, for the majority of requests which just require getting a model from + * the success case and the status code of the error, this can be quite convenient. + */ +sealed class NetworkResult { + companion object { + /** + * A convenience method to capture the common case of making a request. + * Perform the network action in the [fetch] lambda, returning your result. + * Common exceptions will be caught and translated to errors. + */ + fun fromFetch(fetch: () -> T): NetworkResult = try { + Success(fetch()) + } catch (e: NonSuccessfulResponseCodeException) { + StatusCodeError(e.code, e) + } catch (e: IOException) { + NetworkError(e) + } catch (e: Throwable) { + ApplicationError(e) + } + } + + /** Indicates the request was successful */ + data class Success(val result: T) : NetworkResult() + + /** Indicates a generic network error occurred before we were able to process a response. */ + data class NetworkError(val throwable: Throwable? = null) : NetworkResult() + + /** Indicates we got a response, but it was a non-2xx response. */ + data class StatusCodeError(val code: Int, val throwable: Throwable? = null) : NetworkResult() + + /** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */ + data class ApplicationError(val throwable: Throwable) : NetworkResult() + + /** + * Returns the result if successful, otherwise turns the result back into an exception and throws it. + */ + fun successOrThrow(): T { + when (this) { + is Success -> return result + is NetworkError -> throw throwable ?: PushNetworkException("Network error") + is StatusCodeError -> throw throwable ?: NonSuccessfulResponseCodeException(this.code) + is ApplicationError -> throw throwable + } + } + + /** + * Takes the output of one [NetworkResult] and transforms it into another if the operation is successful. + * If it's a failure, the original failure will be propagated. Useful for changing the type of a result. + */ + fun map(transform: (T) -> R): NetworkResult { + return when (this) { + is Success -> Success(transform(this.result)) + is NetworkError -> NetworkError(throwable) + is StatusCodeError -> StatusCodeError(code, throwable) + is ApplicationError -> ApplicationError(throwable) + } + } + + /** + * Takes the output of one [NetworkResult] and passes it as the input to another if the operation is successful. + * If it's a failure, the original failure will be propagated. Useful for chaining operations together. + */ + fun then(result: (T) -> NetworkResult): NetworkResult { + return when (this) { + is Success -> result(this.result) + is NetworkError -> NetworkError(throwable) + is StatusCodeError -> StatusCodeError(code, throwable) + is ApplicationError -> ApplicationError(throwable) + } + } +} 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 b68d5f483a..bf600b4d79 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 @@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest; 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.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; @@ -834,6 +835,10 @@ public class SignalServiceAccountManager { return new GroupsV2Api(pushServiceSocket, groupsV2Operations); } + public ArchiveApi getArchiveApi() { + return ArchiveApi.create(pushServiceSocket, configuration.getBackupServerPublicParams(), credentials.getAci()); + } + public AuthCredentials getPaymentsAuthorization() throws IOException { return pushServiceSocket.getPaymentsAuthorization(); } 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 new file mode 100644 index 0000000000..29a5790b63 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveApi.kt @@ -0,0 +1,139 @@ +/* + * 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.backup.BackupKey +import org.whispersystems.signalservice.api.push.ServiceId.ACI +import org.whispersystems.signalservice.internal.push.PushServiceSocket + +/** + * 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 + ) + } + } + + /** + * 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. + */ + fun getServiceCredentials(currentTime: Long): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.getArchiveCredentials(currentTime) + } + } + + /** + * 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) + } + } + + /** + * 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()) + } + } + + /** + * 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()) + } + } + + /** + * 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()) + } + } + + private fun getZkCredential(backupKey: BackupKey, serviceCredential: ArchiveServiceCredential): BackupAuthCredential { + val backupAuthResponse = BackupAuthCredentialResponse(serviceCredential.credential) + val backupRequestContext = BackupAuthCredentialRequestContext.create(backupKey.value, aci.rawUuid) + + return backupRequestContext.receiveResponse( + backupAuthResponse, + backupServerPublicParams, + 10 + ) + } + + 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 + ) + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveCredentialPresentation.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveCredentialPresentation.kt new file mode 100644 index 0000000000..de6e4f585b --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveCredentialPresentation.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +/** + * Acts as credentials for various archive operations. + */ +class ArchiveCredentialPresentation( + val presentation: ByteArray, + val signedPresentation: ByteArray +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveGetBackupInfoResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveGetBackupInfoResponse.kt new file mode 100644 index 0000000000..399e89e757 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveGetBackupInfoResponse.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Represents the response when fetching the archive backup info. + */ +data class ArchiveGetBackupInfoResponse( + @JsonProperty + val cdn: Int?, + @JsonProperty + val backupDir: String?, + @JsonProperty + val backupName: String?, + @JsonProperty + val usedSpace: Long? +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMessageBackupUploadFormResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMessageBackupUploadFormResponse.kt new file mode 100644 index 0000000000..7b0d102bb3 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveMessageBackupUploadFormResponse.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Represents the response body when we ask for a message backup upload form. + */ +data class ArchiveMessageBackupUploadFormResponse( + @JsonProperty + val cdn: Int, + @JsonProperty + val key: String, + @JsonProperty + val headers: Map, + @JsonProperty + val signedUploadLocation: String +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredential.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredential.kt new file mode 100644 index 0000000000..13299a795e --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredential.kt @@ -0,0 +1,15 @@ +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Represents an individual credential for an archive operation. Note that is isn't the final + * credential you will actually use -- that's [org.signal.libsignal.zkgroup.backups.BackupAuthCredential]. + * But you use these to make those. + */ +class ArchiveServiceCredential( + @JsonProperty + val credential: ByteArray, + @JsonProperty + val redemptionTime: Long +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt new file mode 100644 index 0000000000..bb470f1c28 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveServiceCredentialsResponse.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Represents the result of fetching archive credentials. + * See [ArchiveServiceCredential]. + */ +class ArchiveServiceCredentialsResponse( + @JsonProperty + val credentials: Array +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt new file mode 100644 index 0000000000..38a50c9474 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetBackupIdRequest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import org.signal.core.util.Base64 +import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest + +/** + * Represents the request body when setting the archive backupId. + */ +class ArchiveSetBackupIdRequest( + @JsonProperty + @JsonSerialize(using = BackupAuthCredentialRequestSerializer::class) + val backupAuthCredentialRequest: BackupAuthCredentialRequest +) { + class BackupAuthCredentialRequestSerializer : JsonSerializer() { + override fun serialize(value: BackupAuthCredentialRequest, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(Base64.encodeWithPadding(value.serialize())) + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetPublicKeyRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetPublicKeyRequest.kt new file mode 100644 index 0000000000..89277d2727 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/archive/ArchiveSetPublicKeyRequest.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.archive + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import org.signal.core.util.Base64 +import org.signal.libsignal.protocol.ecc.ECPublicKey + +/** + * Represents the request body when setting the archive public key. + */ +class ArchiveSetPublicKeyRequest( + @JsonProperty + @JsonSerialize(using = PublicKeySerializer::class) + val backupIdPublicKey: ECPublicKey +) { + class PublicKeySerializer : JsonSerializer() { + override fun serialize(value: ECPublicKey, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(Base64.encodeWithPadding(value.serialize())) + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt index bf2a7013da..caf3abfe44 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt @@ -17,5 +17,6 @@ class SignalServiceConfiguration( val dns: Optional, val signalProxy: Optional, val zkGroupServerPublicParams: ByteArray, - val genericServerPublicParams: ByteArray + val genericServerPublicParams: ByteArray, + val backupServerPublicParams: ByteArray ) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 020c59e957..acc954bcfa 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -20,6 +20,7 @@ import org.signal.libsignal.protocol.util.Pair; import org.signal.libsignal.usernames.BaseUsernameException; import org.signal.libsignal.usernames.Username; import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest; import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialResponse; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; @@ -42,6 +43,12 @@ import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest; import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyUpload; +import org.whispersystems.signalservice.api.archive.ArchiveCredentialPresentation; +import org.whispersystems.signalservice.api.archive.ArchiveServiceCredentialsResponse; +import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse; +import org.whispersystems.signalservice.api.archive.ArchiveMessageBackupUploadFormResponse; +import org.whispersystems.signalservice.api.archive.ArchiveSetBackupIdRequest; +import org.whispersystems.signalservice.api.archive.ArchiveSetPublicKeyRequest; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; @@ -85,7 +92,6 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.push.exceptions.RangeException; import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException; -import org.whispersystems.signalservice.api.push.exceptions.RemoteAttestationResponseExpiredException; import org.whispersystems.signalservice.api.push.exceptions.ResumeLocationInvalidException; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; @@ -108,9 +114,6 @@ import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalUrl; -import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; -import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; -import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import org.whispersystems.signalservice.internal.crypto.AttachmentDigest; import org.whispersystems.signalservice.internal.push.exceptions.DonationProcessorError; import org.whispersystems.signalservice.internal.push.exceptions.DonationReceiptCredentialError; @@ -303,6 +306,12 @@ public class PushServiceSocket { private static final String BACKUP_AUTH_CHECK = "/v2/backup/auth/check"; + private static final String ARCHIVE_CREDENTIALS = "/v1/archives/auth?redemptionStartSeconds=%d&redemptionEndSeconds=%d"; + private static final String ARCHIVE_BACKUP_ID = "/v1/archives/backupid"; + private static final String ARCHIVE_PUBLIC_KEY = "/v1/archives/keys"; + private static final String ARCHIVE_INFO = "/v1/archives"; + private static final String ARCHIVE_MESSAGE_UPLOAD_FORM = "/v1/archives/upload/form"; + private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth"; private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; @@ -471,6 +480,48 @@ public class PushServiceSocket { return credentials; } + public ArchiveServiceCredentialsResponse getArchiveCredentials(long currentTime) throws IOException { + long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime)); + long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7); + + String response = makeServiceRequest(String.format(Locale.US, ARCHIVE_CREDENTIALS, secondsRoundedToNearestDay, endTimeInSeconds), "GET", null); + + return JsonUtil.fromJson(response, ArchiveServiceCredentialsResponse.class); + } + + public void setArchiveBackupId(BackupAuthCredentialRequest request) throws IOException { + String body = JsonUtil.toJson(new ArchiveSetBackupIdRequest(request)); + makeServiceRequest(ARCHIVE_BACKUP_ID, "PUT", body); + } + + public void setArchivePublicKey(ECPublicKey publicKey, ArchiveCredentialPresentation credentialPresentation) throws IOException { + Map headers = new HashMap<>(); + headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation())); + headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation())); + + String body = JsonUtil.toJson(new ArchiveSetPublicKeyRequest(publicKey)); + makeServiceRequestWithoutAuthentication(ARCHIVE_PUBLIC_KEY, "PUT", body, headers, NO_HANDLER); + } + + public ArchiveGetBackupInfoResponse getArchiveBackupInfo(ArchiveCredentialPresentation credentialPresentation) throws IOException { + Map headers = new HashMap<>(); + headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation())); + headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation())); + + String response = makeServiceRequestWithoutAuthentication(ARCHIVE_INFO, "GET", null, headers, NO_HANDLER); + return JsonUtil.fromJson(response, ArchiveGetBackupInfoResponse.class); + } + + public ArchiveMessageBackupUploadFormResponse getArchiveMessageBackupUploadForm(ArchiveCredentialPresentation credentialPresentation) throws IOException { + Map headers = new HashMap<>(); + headers.put("X-Signal-ZK-Auth", Base64.encodeWithPadding(credentialPresentation.getPresentation())); + headers.put("X-Signal-ZK-Auth-Signature", Base64.encodeWithPadding(credentialPresentation.getSignedPresentation())); + + String response = makeServiceRequestWithoutAuthentication(ARCHIVE_MESSAGE_UPLOAD_FORM, "GET", null, headers, NO_HANDLER); + return JsonUtil.fromJson(response, ArchiveMessageBackupUploadFormResponse.class); + } + + public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) throws IOException {