mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 20:24:32 +01:00
Add mostly-working SVR3 implementation behind flag.
This commit is contained in:
committed by
Alex Hart
parent
143a61e312
commit
664c22d8f1
@@ -129,7 +129,7 @@ sealed class NetworkResult<T>(
|
||||
|
||||
/**
|
||||
* Will perform an operation if the result at this point in the chain is successful. Note that it runs if the chain is _currently_ successful. It does not
|
||||
* depend on anything futher down the chain.
|
||||
* depend on anything further down the chain.
|
||||
*
|
||||
* ```kotlin
|
||||
* val networkResult: NetworkResult<MyData> = NetworkResult
|
||||
|
||||
@@ -57,6 +57,7 @@ import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2;
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3;
|
||||
import org.whispersystems.signalservice.api.svr.SvrApi;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
@@ -64,7 +65,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckRequest;
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse;
|
||||
import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse;
|
||||
import org.whispersystems.signalservice.internal.push.CdsiAuthResponse;
|
||||
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
|
||||
import org.whispersystems.signalservice.internal.push.PaymentAddress;
|
||||
@@ -183,8 +184,8 @@ public class SignalServiceAccountManager {
|
||||
return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket);
|
||||
}
|
||||
|
||||
public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network, SecureValueRecoveryV3.ShareSetStorage storage) {
|
||||
return new SecureValueRecoveryV3(network, pushServiceSocket, storage);
|
||||
public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network) {
|
||||
return new SecureValueRecoveryV3(network, pushServiceSocket);
|
||||
}
|
||||
|
||||
public WhoAmIResponse getWhoAmI() throws IOException {
|
||||
@@ -205,9 +206,9 @@ public class SignalServiceAccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List<String> usernamePasswords) {
|
||||
public Single<ServiceResponse<BackupV2AuthCheckResponse>> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List<String> usernamePasswords) {
|
||||
|
||||
return pushServiceSocket.checkBackupAuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupAuthCheckResponse.class));
|
||||
return pushServiceSocket.checkSvr2AuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupV2AuthCheckResponse.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -879,6 +880,10 @@ public class SignalServiceAccountManager {
|
||||
return new RegistrationApi(pushServiceSocket);
|
||||
}
|
||||
|
||||
public SvrApi getSvrApi() {
|
||||
return new SvrApi(pushServiceSocket);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return pushServiceSocket.getPaymentsAuthorization();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.account.PreKeyCollection
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse
|
||||
import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse
|
||||
import org.whispersystems.signalservice.internal.push.BackupV3AuthCheckResponse
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
@@ -103,13 +104,24 @@ class RegistrationApi(
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an SVR auth credential that corresponds with the supplied username and password.
|
||||
* Validates the provided SVR2 auth credentials, returning information on their usability.
|
||||
*
|
||||
* `POST /v2/backup/auth/check`
|
||||
*/
|
||||
fun getSvrAuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupAuthCheckResponse> {
|
||||
fun validateSvr2AuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupV2AuthCheckResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords)
|
||||
pushServiceSocket.checkSvr2AuthCredentials(e164, usernamePasswords)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the provided SVR3 auth credentials, returning information on their usability.
|
||||
*
|
||||
* `POST /v3/backup/auth/check`
|
||||
*/
|
||||
fun validateSvr3AuthCredential(e164: String, usernamePasswords: List<String>): NetworkResult<BackupV3AuthCheckResponse> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.checkSvr3AuthCredentials(e164, usernamePasswords)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
interface SecureValueRecovery {
|
||||
|
||||
/**
|
||||
* Which version of SVR this is running against.
|
||||
*/
|
||||
val svrVersion: SvrVersion
|
||||
|
||||
/**
|
||||
* Begins a PIN change.
|
||||
*
|
||||
@@ -38,12 +44,14 @@ interface SecureValueRecovery {
|
||||
* Currently, this will only happen during a reglock challenge. When in this state, the user is not registered, and will instead
|
||||
* be provided credentials in a service response to give the user an opportunity to restore SVR data and generate the reglock proof.
|
||||
*
|
||||
* If the user is already registered, use [restoreDataPostRegistration]
|
||||
* If the user is already registered, use [restoreDataPostRegistration].
|
||||
*
|
||||
* @param shareSet Only used for SVR3, where the value is required. For SVR2, this should be null.
|
||||
*/
|
||||
fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse
|
||||
fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse
|
||||
|
||||
/**
|
||||
* Restores data from SVR. Only intended to be called if the user is already registered. If the user is not yet registered, use [restoreDataPreRegistration]
|
||||
* Restores data from SVR. Only intended to be called if the user is already registered. If the user is not yet registered, use [restoreDataPreRegistration].
|
||||
*/
|
||||
fun restoreDataPostRegistration(userPin: String): RestoreResponse
|
||||
|
||||
@@ -66,7 +74,7 @@ interface SecureValueRecovery {
|
||||
/** Response for setting a PIN. */
|
||||
sealed class BackupResponse {
|
||||
/** Operation completed successfully. */
|
||||
data class Success(val masterKey: MasterKey, val authorization: AuthCredentials) : BackupResponse()
|
||||
data class Success(val masterKey: MasterKey, val authorization: AuthCredentials, val svrVersion: SvrVersion) : BackupResponse()
|
||||
|
||||
/** The operation failed because the server was unable to expose the backup data we created. There is no further action that can be taken besides logging the error and treating it as a success. */
|
||||
object ExposeFailure : BackupResponse()
|
||||
@@ -122,4 +130,9 @@ interface SecureValueRecovery {
|
||||
|
||||
/** Exception indicating that we received a response from the service that our request was invalid. */
|
||||
class InvalidRequestException(message: String) : Exception(message)
|
||||
|
||||
enum class SvrVersion {
|
||||
SVR2,
|
||||
SVR3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteRespon
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.InvalidRequestException
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.SvrVersion
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
@@ -43,6 +44,8 @@ class SecureValueRecoveryV2(
|
||||
private val TAG = SecureValueRecoveryV2::class.java.simpleName
|
||||
}
|
||||
|
||||
override val svrVersion: SvrVersion = SvrVersion.SVR2
|
||||
|
||||
override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return Svr2PinChangeSession(userPin, masterKey)
|
||||
}
|
||||
@@ -57,7 +60,7 @@ class SecureValueRecoveryV2(
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse {
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse {
|
||||
return restoreData({ authorization }, userPin)
|
||||
}
|
||||
|
||||
@@ -89,7 +92,7 @@ class SecureValueRecoveryV2(
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authorization(): AuthCredentials {
|
||||
return pushServiceSocket.svrAuthorization
|
||||
return pushServiceSocket.svr2Authorization
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -249,7 +252,7 @@ class SecureValueRecoveryV2(
|
||||
.let { response ->
|
||||
when (response.expose?.status) {
|
||||
ProtoExposeResponse.Status.OK -> {
|
||||
BackupResponse.Success(masterKey, authorization)
|
||||
BackupResponse.Success(masterKey, authorization, SvrVersion.SVR2)
|
||||
}
|
||||
ProtoExposeResponse.Status.ERROR -> {
|
||||
BackupResponse.ExposeFailure
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.attest.AttestationFailedException
|
||||
import org.signal.libsignal.net.EnclaveAuth
|
||||
@@ -20,8 +23,12 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupRespon
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.SvrVersion
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64
|
||||
import org.whispersystems.signalservice.internal.push.ByteArraySerializerBase64NoPadding
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutionException
|
||||
@@ -31,28 +38,100 @@ import java.util.concurrent.ExecutionException
|
||||
*/
|
||||
class SecureValueRecoveryV3(
|
||||
private val network: Network,
|
||||
private val pushServiceSocket: PushServiceSocket,
|
||||
private val shareSetStorage: ShareSetStorage
|
||||
private val pushServiceSocket: PushServiceSocket
|
||||
) : SecureValueRecovery {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(SecureValueRecoveryV3::class)
|
||||
}
|
||||
|
||||
override val svrVersion: SvrVersion = SvrVersion.SVR3
|
||||
|
||||
override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return Svr3PinChangeSession(userPin, masterKey)
|
||||
return Svr3PinChangeSession(userPin, masterKey, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Unlike SVR2, there is no concept of "resuming", so this is equivalent to starting a new session.
|
||||
*/
|
||||
override fun resumePinChangeSession(userPin: String, masterKey: MasterKey, serializedChangeSession: String): PinChangeSession {
|
||||
return Svr3PinChangeSession(userPin, masterKey)
|
||||
val data: Svr3SessionData = JsonUtil.fromJson(serializedChangeSession, Svr3SessionData::class.java)
|
||||
|
||||
return if (data.userPin == userPin && data.masterKey == masterKey) {
|
||||
Svr3PinChangeSession(userPin, masterKey, data.shareSet)
|
||||
} else {
|
||||
Svr3PinChangeSession(userPin, masterKey, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse {
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse {
|
||||
return restoreData(authorization, shareSet, userPin)
|
||||
}
|
||||
|
||||
override fun restoreDataPostRegistration(userPin: String): RestoreResponse {
|
||||
val authorization: Svr3Credentials = try {
|
||||
pushServiceSocket.svr3Authorization
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
return RestoreResponse.ApplicationError(e)
|
||||
} catch (e: IOException) {
|
||||
return RestoreResponse.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
return RestoreResponse.ApplicationError(e)
|
||||
}
|
||||
|
||||
return restoreData(authorization.toAuthCredential(), authorization.shareSet, userPin)
|
||||
}
|
||||
|
||||
/**
|
||||
* There's no concept of "deleting" data with SVR3.
|
||||
*/
|
||||
override fun deleteData(): DeleteResponse {
|
||||
val authorization: Svr3Credentials = try {
|
||||
pushServiceSocket.svr3Authorization
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
return DeleteResponse.ApplicationError(e)
|
||||
} catch (e: IOException) {
|
||||
return DeleteResponse.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
return DeleteResponse.ApplicationError(e)
|
||||
}
|
||||
|
||||
val enclaveAuth = EnclaveAuth(authorization.username, authorization.password)
|
||||
|
||||
return try {
|
||||
network.svr3().remove(enclaveAuth).get()
|
||||
DeleteResponse.Success
|
||||
} catch (e: ExecutionException) {
|
||||
when (val cause = e.cause) {
|
||||
is NetworkException -> DeleteResponse.NetworkError(cause)
|
||||
is AttestationFailedException -> DeleteResponse.ApplicationError(cause)
|
||||
is SgxCommunicationFailureException -> DeleteResponse.ApplicationError(cause)
|
||||
is IOException -> DeleteResponse.NetworkError(cause)
|
||||
else -> DeleteResponse.ApplicationError(cause ?: RuntimeException("Unknown!"))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
DeleteResponse.ApplicationError(e)
|
||||
} catch (e: CancellationException) {
|
||||
DeleteResponse.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authorization(): AuthCredentials {
|
||||
return pushServiceSocket.svr3Authorization.toAuthCredential()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "SVR3"
|
||||
}
|
||||
|
||||
private fun restoreData(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse {
|
||||
if (shareSet == null) {
|
||||
Log.w(TAG, "No share set provided! Assuming no data to restore.")
|
||||
return RestoreResponse.Missing
|
||||
}
|
||||
|
||||
val normalizedPin: String = PinHashUtil.normalizeToString(userPin)
|
||||
val shareSet = shareSetStorage.read() ?: return RestoreResponse.ApplicationError(IllegalStateException("No share set found!"))
|
||||
val enclaveAuth = EnclaveAuth(authorization.username(), authorization.password())
|
||||
|
||||
return try {
|
||||
@@ -76,40 +155,22 @@ class SecureValueRecoveryV3(
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreDataPostRegistration(userPin: String): RestoreResponse {
|
||||
val authorization: AuthCredentials = try {
|
||||
pushServiceSocket.svrAuthorization
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
return RestoreResponse.ApplicationError(e)
|
||||
} catch (e: IOException) {
|
||||
return RestoreResponse.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
return RestoreResponse.ApplicationError(e)
|
||||
}
|
||||
|
||||
return restoreDataPreRegistration(authorization, userPin)
|
||||
}
|
||||
|
||||
/**
|
||||
* There's no concept of "deleting" data with SVR3.
|
||||
*/
|
||||
override fun deleteData(): DeleteResponse {
|
||||
return DeleteResponse.Success
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authorization(): AuthCredentials {
|
||||
return pushServiceSocket.svrAuthorization
|
||||
private fun Svr3Credentials.toAuthCredential(): AuthCredentials {
|
||||
return AuthCredentials.create(username, password)
|
||||
}
|
||||
|
||||
inner class Svr3PinChangeSession(
|
||||
private val userPin: String,
|
||||
private val masterKey: MasterKey
|
||||
private val masterKey: MasterKey,
|
||||
private var shareSet: ByteArray?
|
||||
) : PinChangeSession {
|
||||
|
||||
/**
|
||||
* Performs the PIN change operation. This is safe to call repeatedly if you get back a retryable error.
|
||||
*/
|
||||
override fun execute(): BackupResponse {
|
||||
val normalizedPin: String = PinHashUtil.normalizeToString(userPin)
|
||||
val rawAuth = try {
|
||||
pushServiceSocket.svrAuthorization
|
||||
val rawAuth: Svr3Credentials = try {
|
||||
pushServiceSocket.svr3Authorization
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
return BackupResponse.ApplicationError(e)
|
||||
} catch (e: IOException) {
|
||||
@@ -118,38 +179,56 @@ class SecureValueRecoveryV3(
|
||||
return BackupResponse.ApplicationError(e)
|
||||
}
|
||||
|
||||
val enclaveAuth = EnclaveAuth(rawAuth.username(), rawAuth.password())
|
||||
if (shareSet == null) {
|
||||
val normalizedPin: String = PinHashUtil.normalizeToString(userPin)
|
||||
val enclaveAuth = EnclaveAuth(rawAuth.username, rawAuth.password)
|
||||
|
||||
try {
|
||||
shareSet = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get()
|
||||
} catch (e: ExecutionException) {
|
||||
when (val cause = e.cause) {
|
||||
is NetworkException -> BackupResponse.NetworkError(cause)
|
||||
is AttestationFailedException -> BackupResponse.ApplicationError(cause)
|
||||
is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause)
|
||||
is IOException -> BackupResponse.NetworkError(cause)
|
||||
else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!"))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
} catch (e: CancellationException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
val result = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get()
|
||||
shareSetStorage.write(result)
|
||||
BackupResponse.Success(masterKey, rawAuth)
|
||||
} catch (e: ExecutionException) {
|
||||
when (val cause = e.cause) {
|
||||
is NetworkException -> BackupResponse.NetworkError(cause)
|
||||
is AttestationFailedException -> BackupResponse.ApplicationError(cause)
|
||||
is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause)
|
||||
is IOException -> BackupResponse.NetworkError(cause)
|
||||
else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!"))
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
} catch (e: CancellationException) {
|
||||
pushServiceSocket.setShareSet(shareSet)
|
||||
BackupResponse.Success(masterKey, pushServiceSocket.svr3Authorization.toAuthCredential(), SvrVersion.SVR3)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
} catch (e: IOException) {
|
||||
BackupResponse.NetworkError(e)
|
||||
} catch (e: Exception) {
|
||||
return BackupResponse.ApplicationError(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(): String {
|
||||
// There is no "resuming" SVR3, so we don't need to serialize anything
|
||||
return ""
|
||||
return JsonUtil.toJson(Svr3SessionData(userPin, masterKey, shareSet))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface to allow reading and writing the "share set" to persistent storage.
|
||||
*/
|
||||
interface ShareSetStorage {
|
||||
fun write(data: ByteArray)
|
||||
fun read(): ByteArray?
|
||||
}
|
||||
class Svr3SessionData(
|
||||
@JsonProperty
|
||||
val userPin: String,
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = JsonUtil.MasterKeySerializer::class)
|
||||
@JsonDeserialize(using = JsonUtil.MasterKeyDeserializer::class)
|
||||
val masterKey: MasterKey,
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializerBase64NoPadding::class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializerBase64::class)
|
||||
val shareSet: ByteArray?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import org.whispersystems.signalservice.internal.push.ByteArraySerializerBase64NoPadding
|
||||
|
||||
/**
|
||||
* Request body for setting a share-set on the service.
|
||||
*/
|
||||
class SetShareSetRequest(
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializerBase64NoPadding::class)
|
||||
val shareSet: ByteArray
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64
|
||||
|
||||
/**
|
||||
* Response object when fetching SVR3 auth credentials. We also use it elsewhere as a convenient container
|
||||
* for the (username, password, shareset) tuple.
|
||||
*/
|
||||
class Svr3Credentials(
|
||||
@JsonProperty
|
||||
val username: String,
|
||||
|
||||
@JsonProperty
|
||||
val password: String,
|
||||
|
||||
@JsonProperty
|
||||
@JsonDeserialize(using = ByteArrayDeserializerBase64::class)
|
||||
val shareSet: ByteArray?
|
||||
) {
|
||||
val authCredentials: AuthCredentials
|
||||
get() = AuthCredentials.create(username, password)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
|
||||
class SvrApi(private val pushServiceSocket: PushServiceSocket) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun create(pushServiceSocket: PushServiceSocket): SvrApi {
|
||||
return SvrApi(pushServiceSocket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the latest share-set on the service. The share-set is a piece of data generated in the course of setting a PIN on SVR3 that needs to be
|
||||
*/
|
||||
fun setShareSet(shareSet: ByteArray): NetworkResult<Unit> {
|
||||
return NetworkResult.fromFetch {
|
||||
pushServiceSocket.setShareSet(shareSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
|
||||
/**
|
||||
* Processes a response from the verify stored KBS auth credentials request.
|
||||
*/
|
||||
class BackupAuthCheckProcessor(response: ServiceResponse<BackupV2AuthCheckResponse>) : ServiceResponseProcessor<BackupV2AuthCheckResponse>(response) {
|
||||
fun getInvalid(): List<String> {
|
||||
return response.result.map { it.invalid }.orElse(emptyList())
|
||||
}
|
||||
|
||||
fun hasValidSvr2AuthCredential(): Boolean {
|
||||
return response.result.map { it.match }.orElse(null) != null
|
||||
}
|
||||
|
||||
fun requireSvr2AuthCredential(): AuthCredentials {
|
||||
return response.result.get().match!!
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,14 @@
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
/**
|
||||
* Request body JSON for verifying stored KBS auth credentials.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class BackupAuthCheckRequest @JsonCreator constructor(
|
||||
class BackupAuthCheckRequest(
|
||||
@JsonProperty
|
||||
val number: String?,
|
||||
|
||||
@JsonProperty
|
||||
val passwords: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify KBS auth credentials JSON response.
|
||||
*/
|
||||
data class BackupAuthCheckResponse @JsonCreator constructor(
|
||||
private val matches: Map<String, Map<String, Any>>
|
||||
) {
|
||||
private val actualMatches = matches["matches"] ?: emptyMap()
|
||||
|
||||
val match: AuthCredentials? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toAuthCredential()
|
||||
val invalid: List<String> = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() }
|
||||
|
||||
/** Server expects and returns values as <username>:<password> but we prefer the full encoded Basic auth header format */
|
||||
private fun String.toBasic(): String {
|
||||
return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}"
|
||||
}
|
||||
|
||||
private fun String.toAuthCredential(): AuthCredentials {
|
||||
val firstColonIndex = this.indexOf(":")
|
||||
|
||||
if (firstColonIndex < 0) {
|
||||
throw IOException("Invalid credential returned!")
|
||||
}
|
||||
|
||||
val username = this.substring(0, firstColonIndex)
|
||||
val password = this.substring(firstColonIndex + 1)
|
||||
|
||||
return AuthCredentials.create(username, password)
|
||||
}
|
||||
|
||||
fun merge(other: BackupAuthCheckResponse): BackupAuthCheckResponse {
|
||||
return BackupAuthCheckResponse(this.matches + other.matches)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a response from the verify stored KBS auth credentials request.
|
||||
*/
|
||||
class BackupAuthCheckProcessor(response: ServiceResponse<BackupAuthCheckResponse>) : ServiceResponseProcessor<BackupAuthCheckResponse>(response) {
|
||||
fun getInvalid(): List<String> {
|
||||
return response.result.map { it.invalid }.orElse(emptyList())
|
||||
}
|
||||
|
||||
fun hasValidSvr2AuthCredential(): Boolean {
|
||||
return response.result.map { it.match }.orElse(null) != null
|
||||
}
|
||||
|
||||
fun requireSvr2AuthCredential(): AuthCredentials {
|
||||
return response.result.get().match!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator
|
||||
import okio.ByteString.Companion.encode
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Verify KBS auth credentials JSON response.
|
||||
*/
|
||||
data class BackupV2AuthCheckResponse @JsonCreator constructor(
|
||||
private val matches: Map<String, Map<String, Any>>
|
||||
) {
|
||||
private val actualMatches = matches["matches"] ?: emptyMap()
|
||||
|
||||
val match: AuthCredentials? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toAuthCredential()
|
||||
val invalid: List<String> = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() }
|
||||
|
||||
/** Server expects and returns values as <username>:<password> but we prefer the full encoded Basic auth header format */
|
||||
private fun String.toBasic(): String {
|
||||
return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}"
|
||||
}
|
||||
|
||||
private fun String.toAuthCredential(): AuthCredentials {
|
||||
val firstColonIndex = this.indexOf(":")
|
||||
|
||||
if (firstColonIndex < 0) {
|
||||
throw IOException("Invalid credential returned!")
|
||||
}
|
||||
|
||||
val username = this.substring(0, firstColonIndex)
|
||||
val password = this.substring(firstColonIndex + 1)
|
||||
|
||||
return AuthCredentials.create(username, password)
|
||||
}
|
||||
|
||||
fun merge(other: BackupV2AuthCheckResponse): BackupV2AuthCheckResponse {
|
||||
return BackupV2AuthCheckResponse(this.matches + other.matches)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.internal.push
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Response when verifying whether we have valid SVR3 credentials.
|
||||
*
|
||||
* We use a map here because the keys are dynamic. The response looks like this:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "matches": {
|
||||
* "username:password": {
|
||||
* "status": "match",
|
||||
* "shareSet": "<base64Data>"
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
class BackupV3AuthCheckResponse(
|
||||
@JsonProperty
|
||||
private val matches: Map<String, MatchData>
|
||||
) {
|
||||
|
||||
/** A response that contains a valid SVR3 auth credential if one is present, else null. */
|
||||
val match: Svr3Credentials? = matches.entries.firstOrNull { it.value.isMatch }?.let {
|
||||
val credential = it.key.toAuthCredential()
|
||||
Svr3Credentials(credential.username(), credential.password(), it.value.shareSet)
|
||||
}
|
||||
|
||||
/** A list of credentials that are invalid, in basic-auth format. */
|
||||
val invalid: List<String> = matches.filterValues { it.isInvalid }.keys.map { it.toBasic() }
|
||||
|
||||
private fun String.toAuthCredential(): AuthCredentials {
|
||||
val firstColonIndex = this.indexOf(":")
|
||||
|
||||
if (firstColonIndex < 0) {
|
||||
throw IOException("Invalid credential returned!")
|
||||
}
|
||||
|
||||
val username = this.substring(0, firstColonIndex)
|
||||
val password = this.substring(firstColonIndex + 1)
|
||||
|
||||
return AuthCredentials.create(username, password)
|
||||
}
|
||||
|
||||
/** Server expects and returns values as <username>:<password> but we prefer the full encoded Basic auth header format */
|
||||
private fun String.toBasic(): String {
|
||||
return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}"
|
||||
}
|
||||
|
||||
class MatchData(
|
||||
@JsonProperty
|
||||
val status: String,
|
||||
|
||||
@JsonProperty
|
||||
@JsonDeserialize(using = ByteArrayDeserializerBase64::class)
|
||||
val shareSet: ByteArray
|
||||
) {
|
||||
val isMatch = status == "match"
|
||||
val isInvalid = status == "invalid"
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,21 @@ package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials;
|
||||
|
||||
public final class LockedException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
private final int length;
|
||||
private final long timeRemaining;
|
||||
private final AuthCredentials svr1Credentials;
|
||||
private final AuthCredentials svr2Credentials;
|
||||
private final Svr3Credentials svr3Credentials;
|
||||
|
||||
public LockedException(int length, long timeRemaining, AuthCredentials svr1Credentials, AuthCredentials svr2Credentials) {
|
||||
public LockedException(int length, long timeRemaining, AuthCredentials svr2Credentials, Svr3Credentials svr3Credentials) {
|
||||
super(423);
|
||||
this.length = length;
|
||||
this.timeRemaining = timeRemaining;
|
||||
this.svr1Credentials = svr1Credentials;
|
||||
this.svr2Credentials = svr2Credentials;
|
||||
this.svr3Credentials = svr3Credentials;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
@@ -26,11 +27,11 @@ public final class LockedException extends NonSuccessfulResponseCodeException {
|
||||
return timeRemaining;
|
||||
}
|
||||
|
||||
public AuthCredentials getSvr1Credentials() {
|
||||
return svr1Credentials;
|
||||
}
|
||||
|
||||
public AuthCredentials getSvr2Credentials() {
|
||||
return svr2Credentials;
|
||||
}
|
||||
|
||||
public Svr3Credentials getSvr3Credentials() {
|
||||
return svr3Credentials;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,8 @@ import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIn
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
|
||||
import org.whispersystems.signalservice.api.svr.SetShareSetRequest;
|
||||
import org.whispersystems.signalservice.api.svr.Svr3Credentials;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory;
|
||||
@@ -306,11 +308,13 @@ public class PushServiceSocket {
|
||||
private static final String REGISTRATION_PATH = "/v1/registration";
|
||||
|
||||
private static final String CDSI_AUTH = "/v2/directory/auth";
|
||||
private static final String SVR_AUTH = "/v2/backup/auth";
|
||||
private static final String SVR2_AUTH = "/v2/backup/auth";
|
||||
private static final String SVR3_AUTH = "/v3/backup/auth";
|
||||
|
||||
private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
|
||||
|
||||
private static final String BACKUP_AUTH_CHECK = "/v2/backup/auth/check";
|
||||
private static final String BACKUP_AUTH_CHECK_V2 = "/v2/backup/auth/check";
|
||||
private static final String BACKUP_AUTH_CHECK_V3 = "/v3/backup/auth/check";
|
||||
|
||||
private static final String ARCHIVE_CREDENTIALS = "/v1/archives/auth?redemptionStartSeconds=%d&redemptionEndSeconds=%d";
|
||||
private static final String ARCHIVE_READ_CREDENTIALS = "/v1/archives/auth/read?cdn=%d";
|
||||
@@ -325,6 +329,8 @@ public class PushServiceSocket {
|
||||
private static final String ARCHIVE_MEDIA_DELETE = "/v1/archives/media/delete";
|
||||
private static final String ARCHIVE_MEDIA_DOWNLOAD_PATH = "backups/%s/%s/%s";
|
||||
|
||||
private static final String SET_SHARE_SET_PATH = "/v3/backup/share-set";
|
||||
|
||||
private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth";
|
||||
private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp";
|
||||
|
||||
@@ -487,13 +493,18 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
||||
}
|
||||
|
||||
public AuthCredentials getSvrAuthorization() throws IOException {
|
||||
String body = makeServiceRequest(SVR_AUTH, "GET", null);
|
||||
public AuthCredentials getSvr2Authorization() throws IOException {
|
||||
String body = makeServiceRequest(SVR2_AUTH, "GET", null);
|
||||
AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class);
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public Svr3Credentials getSvr3Authorization() throws IOException {
|
||||
String body = makeServiceRequest(SVR3_AUTH, "GET", null);
|
||||
return JsonUtil.fromJsonResponse(body, Svr3Credentials.class);
|
||||
}
|
||||
|
||||
public ArchiveServiceCredentialsResponse getArchiveCredentials(long currentTime) throws IOException {
|
||||
long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime));
|
||||
long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7);
|
||||
@@ -609,6 +620,11 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(response, GetArchiveCdnCredentialsResponse.class);
|
||||
}
|
||||
|
||||
public void setShareSet(byte[] shareSet) throws IOException {
|
||||
SetShareSetRequest request = new SetShareSetRequest(shareSet);
|
||||
makeServiceRequest(SET_SHARE_SET_PATH, "PUT", JsonUtil.toJson(request));
|
||||
}
|
||||
|
||||
public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest)
|
||||
throws IOException
|
||||
{
|
||||
@@ -1127,11 +1143,11 @@ public class PushServiceSocket {
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull BackupAuthCheckRequest request,
|
||||
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
||||
public Single<ServiceResponse<BackupV2AuthCheckResponse>> checkSvr2AuthCredentials(@Nonnull BackupAuthCheckRequest request,
|
||||
@Nonnull ResponseMapper<BackupV2AuthCheckResponse> responseMapper)
|
||||
{
|
||||
Single<ServiceResponse<BackupAuthCheckResponse>> requestSingle = Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
|
||||
Single<ServiceResponse<BackupV2AuthCheckResponse>> requestSingle = Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
|
||||
String body = response.body() != null ? readBodyString(response.body()): "";
|
||||
return responseMapper.map(response.code(), body, response::header, false);
|
||||
}
|
||||
@@ -1143,21 +1159,14 @@ public class PushServiceSocket {
|
||||
.onErrorReturn(ServiceResponse::forUnknownError);
|
||||
}
|
||||
|
||||
public BackupAuthCheckResponse checkBackupAuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
return JsonUtil.fromJson(response, BackupAuthCheckResponse.class);
|
||||
public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class);
|
||||
}
|
||||
|
||||
private Single<ServiceResponse<BackupAuthCheckResponse>> createBackupAuthCheckSingle(@Nonnull String path,
|
||||
@Nonnull BackupAuthCheckRequest request,
|
||||
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
||||
{
|
||||
return Single.fromCallable(() -> {
|
||||
try (Response response = getServiceConnection(path, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) {
|
||||
String body = response.body() != null ? readBodyString(response.body()): "";
|
||||
return responseMapper.map(response.code(), body, response::header, false);
|
||||
}
|
||||
});
|
||||
public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List<String> passwords) throws IOException {
|
||||
String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty());
|
||||
return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2243,8 +2252,8 @@ public class PushServiceSocket {
|
||||
|
||||
throw new LockedException(accountLockFailure.length,
|
||||
accountLockFailure.timeRemaining,
|
||||
accountLockFailure.svr1Credentials,
|
||||
accountLockFailure.svr2Credentials);
|
||||
accountLockFailure.svr2Credentials,
|
||||
accountLockFailure.svr3Credentials);
|
||||
case 428:
|
||||
ProofRequiredResponse proofRequiredResponse = readResponseJson(response, ProofRequiredResponse.class);
|
||||
String retryAfterRaw = response.header("Retry-After");
|
||||
@@ -2676,6 +2685,9 @@ public class PushServiceSocket {
|
||||
|
||||
@JsonProperty
|
||||
public AuthCredentials svr2Credentials;
|
||||
|
||||
@JsonProperty
|
||||
public Svr3Credentials svr3Credentials;
|
||||
}
|
||||
|
||||
private static class ConnectionHolder {
|
||||
|
||||
@@ -121,8 +121,8 @@ public final class DefaultErrorMapper implements ErrorMapper {
|
||||
|
||||
return new LockedException(accountLockFailure.length,
|
||||
accountLockFailure.timeRemaining,
|
||||
accountLockFailure.svr1Credentials,
|
||||
accountLockFailure.svr2Credentials);
|
||||
accountLockFailure.svr2Credentials,
|
||||
accountLockFailure.svr3Credentials);
|
||||
case 428:
|
||||
ProofRequiredResponse proofRequiredResponse;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user