Add some initial backupV2 network infrastructure.

This commit is contained in:
Greyson Parrelli 2023-12-07 16:11:57 -05:00 committed by Cody Henthorne
parent e17b07bb12
commit 6230a7553d
20 changed files with 609 additions and 11 deletions

View file

@ -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\"")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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