Add some initial backupV2 network infrastructure.
This commit is contained in:
parent
e17b07bb12
commit
6230a7553d
20 changed files with 609 additions and 11 deletions
|
@ -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\"")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<ArchiveServiceCredential> {
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<String> = 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<ArchiveServiceCredential>) {
|
||||
val current: MutableMap<Long, ArchiveServiceCredential> = 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<Long, ArchiveServiceCredential> = credentialsByDay.toMutableMap()
|
||||
val updated = current.filterKeys { it < startOfDayInSeconds }
|
||||
putString(KEY_CREDENTIALS, JsonUtil.toJson(SerializedCredentials(updated)))
|
||||
}
|
||||
|
||||
class SerializedCredentials(
|
||||
@JsonProperty
|
||||
val credentialsByDay: Map<Long, ArchiveServiceCredential>
|
||||
)
|
||||
|
||||
/**
|
||||
* A [Map] wrapper that makes it easier to get the credential for the current time.
|
||||
*/
|
||||
class ArchiveServiceCredentials(map: Map<Long, ArchiveServiceCredential>) : Map<Long, ArchiveServiceCredential> 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]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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<HostConfig> = 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<Int, SignalServiceConfiguration> = 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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T> {
|
||||
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 <T> fromFetch(fetch: () -> T): NetworkResult<T> = 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<T>(val result: T) : NetworkResult<T>()
|
||||
|
||||
/** Indicates a generic network error occurred before we were able to process a response. */
|
||||
data class NetworkError<T>(val throwable: Throwable? = null) : NetworkResult<T>()
|
||||
|
||||
/** Indicates we got a response, but it was a non-2xx response. */
|
||||
data class StatusCodeError<T>(val code: Int, val throwable: Throwable? = null) : NetworkResult<T>()
|
||||
|
||||
/** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
|
||||
data class ApplicationError<T>(val throwable: Throwable) : NetworkResult<T>()
|
||||
|
||||
/**
|
||||
* 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 <R> map(transform: (T) -> R): NetworkResult<R> {
|
||||
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 <R> then(result: (T) -> NetworkResult<R>): NetworkResult<R> {
|
||||
return when (this) {
|
||||
is Success -> result(this.result)
|
||||
is NetworkError -> NetworkError(throwable)
|
||||
is StatusCodeError -> StatusCodeError(code, throwable)
|
||||
is ApplicationError -> ApplicationError(throwable)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<ArchiveServiceCredentialsResponse> {
|
||||
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<Unit> {
|
||||
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<Unit> {
|
||||
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<ArchiveMessageBackupUploadFormResponse> {
|
||||
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<ArchiveGetBackupInfoResponse> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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?
|
||||
)
|
|
@ -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<String, String>,
|
||||
@JsonProperty
|
||||
val signedUploadLocation: String
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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<ArchiveServiceCredential>
|
||||
)
|
|
@ -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<BackupAuthCredentialRequest>() {
|
||||
override fun serialize(value: BackupAuthCredentialRequest, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(Base64.encodeWithPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ECPublicKey>() {
|
||||
override fun serialize(value: ECPublicKey, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(Base64.encodeWithPadding(value.serialize()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,5 +17,6 @@ class SignalServiceConfiguration(
|
|||
val dns: Optional<Dns>,
|
||||
val signalProxy: Optional<SignalProxy>,
|
||||
val zkGroupServerPublicParams: ByteArray,
|
||||
val genericServerPublicParams: ByteArray
|
||||
val genericServerPublicParams: ByteArray,
|
||||
val backupServerPublicParams: ByteArray
|
||||
)
|
||||
|
|
|
@ -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<String, String> 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<String, String> 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<String, String> 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
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue