Add mostly-working SVR3 implementation behind flag.

This commit is contained in:
Greyson Parrelli
2024-06-07 15:40:53 -04:00
committed by Alex Hart
parent 143a61e312
commit 664c22d8f1
44 changed files with 1008 additions and 313 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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