Convert SVR and GV2 auth requests to WebSocket.

This commit is contained in:
Cody Henthorne
2025-03-14 16:50:39 -04:00
parent 9e9a47f0da
commit 41e0f2193a
10 changed files with 82 additions and 64 deletions

View File

@@ -361,7 +361,7 @@ object AppDependencies {
interface Provider {
fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket
fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations
fun provideSignalServiceAccountManager(authWebSocket: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager
fun provideSignalServiceAccountManager(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, accountApi: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager
fun provideSignalServiceMessageSender(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, protocolStore: SignalServiceDataStore, pushServiceSocket: PushServiceSocket, messageApi: MessageApi): SignalServiceMessageSender
fun provideSignalServiceMessageReceiver(pushServiceSocket: PushServiceSocket): SignalServiceMessageReceiver
fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess

View File

@@ -146,8 +146,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
}
@Override
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull AccountApi accountApi, @NonNull PushServiceSocket pushServiceSocket, @NonNull GroupsV2Operations groupsV2Operations) {
return new SignalServiceAccountManager(accountApi, pushServiceSocket, groupsV2Operations);
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket, @NonNull AccountApi accountApi, @NonNull PushServiceSocket pushServiceSocket, @NonNull GroupsV2Operations groupsV2Operations) {
return new SignalServiceAccountManager(authWebSocket, accountApi, pushServiceSocket, groupsV2Operations);
}
@Override

View File

@@ -95,7 +95,7 @@ class NetworkDependenciesModule(
}
val signalServiceAccountManager: SignalServiceAccountManager by lazy {
provider.provideSignalServiceAccountManager(accountApi, pushServiceSocket, groupsV2Operations)
provider.provideSignalServiceAccountManager(authWebSocket, accountApi, pushServiceSocket, groupsV2Operations)
}
val libsignalNetwork: Network by lazy {

View File

@@ -66,7 +66,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
return mockk(relaxed = true)
}
override fun provideSignalServiceAccountManager(authWebSocket: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager {
override fun provideSignalServiceAccountManager(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, accountApi: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager {
return mockk(relaxed = true)
}

View File

@@ -17,12 +17,9 @@ import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.payments.CurrencyConversions;
import org.whispersystems.signalservice.api.profiles.AvatarUploadParams;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.push.ServiceIdType;
@@ -31,8 +28,8 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.api.registration.RegistrationApi;
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2;
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.AuthCredentials;
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts;
import org.whispersystems.signalservice.internal.push.PaymentAddress;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
@@ -43,7 +40,6 @@ import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputSt
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -53,6 +49,9 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* The main interface for creating, registering, and
* managing a Signal Service account.
@@ -63,10 +62,11 @@ public class SignalServiceAccountManager {
private static final String TAG = SignalServiceAccountManager.class.getSimpleName();
private final PushServiceSocket pushServiceSocket;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceConfiguration configuration;
private final AccountApi accountApi;
private final PushServiceSocket pushServiceSocket;
private final GroupsV2Operations groupsV2Operations;
private final SignalServiceConfiguration configuration;
private final SignalWebSocket.AuthenticatedWebSocket authWebSocket;
private final AccountApi accountApi;
/**
* Construct a SignalServiceAccountManager.
@@ -91,13 +91,18 @@ public class SignalServiceAccountManager {
GroupsV2Operations gv2Operations = new GroupsV2Operations(ClientZkOperations.create(configuration), maxGroupSize);
return new SignalServiceAccountManager(
null,
null,
new PushServiceSocket(configuration, credentialProvider, signalAgent, gv2Operations.getProfileOperations(), automaticNetworkRetry),
gv2Operations
);
}
public SignalServiceAccountManager(AccountApi accountApi, PushServiceSocket pushServiceSocket, GroupsV2Operations groupsV2Operations) {
public SignalServiceAccountManager(@Nullable SignalWebSocket.AuthenticatedWebSocket authWebSocket,
@Nullable AccountApi accountApi,
@Nonnull PushServiceSocket pushServiceSocket,
@Nonnull GroupsV2Operations groupsV2Operations) {
this.authWebSocket = authWebSocket;
this.accountApi = accountApi;
this.groupsV2Operations = groupsV2Operations;
this.pushServiceSocket = pushServiceSocket;
@@ -113,11 +118,11 @@ public class SignalServiceAccountManager {
}
public SecureValueRecoveryV2 getSecureValueRecoveryV2(String mrEnclave) {
return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket);
return new SecureValueRecoveryV2(configuration, mrEnclave, authWebSocket);
}
public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network) {
return new SecureValueRecoveryV3(network, pushServiceSocket);
return new SecureValueRecoveryV3(network, authWebSocket);
}
public WhoAmIResponse getWhoAmI() throws IOException {
@@ -243,7 +248,7 @@ public class SignalServiceAccountManager {
}
public GroupsV2Api getGroupsV2Api() {
return new GroupsV2Api(pushServiceSocket, groupsV2Operations);
return new GroupsV2Api(authWebSocket, pushServiceSocket, groupsV2Operations);
}
public RegistrationApi getRegistrationApi() {

View File

@@ -25,6 +25,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.websocket.SignalWebSocket;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenException;
@@ -43,10 +44,12 @@ import okio.ByteString;
public class GroupsV2Api {
private final PushServiceSocket socket;
private final GroupsV2Operations groupsOperations;
private final SignalWebSocket.AuthenticatedWebSocket authWebSocket;
private final PushServiceSocket socket;
private final GroupsV2Operations groupsOperations;
public GroupsV2Api(PushServiceSocket socket, GroupsV2Operations groupsOperations) {
public GroupsV2Api(SignalWebSocket.AuthenticatedWebSocket authWebSocket, PushServiceSocket socket, GroupsV2Operations groupsOperations) {
this.authWebSocket = authWebSocket;
this.socket = socket;
this.groupsOperations = groupsOperations;
}
@@ -54,10 +57,8 @@ public class GroupsV2Api {
/**
* Provides 7 days of credentials, which you should cache.
*/
public CredentialResponseMaps getCredentials(long todaySeconds)
throws IOException
{
return parseCredentialResponse(socket.retrieveGroupsV2Credentials(todaySeconds));
public CredentialResponseMaps getCredentials(long todaySeconds) throws IOException {
return parseCredentialResponse(GroupsV2ApiHelper.getCredentials(authWebSocket, todaySeconds));
}
/**

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.groupsv2
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.get
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.io.IOException
import kotlin.time.Duration.Companion.days
/**
* Allow [GroupsV2Api] to have a partial kotlin conversion by putting more kotlin friendly calls here.
*/
object GroupsV2ApiHelper {
/**
* Provides 7 days of credentials, which you should cache.
*
* GET /v1/certificate/auth/group?redemptionStartSeconds=[todaySeconds]&redemptionEndSeconds=`todaySecondsPlus7DaysOfSeconds`
* - 200: Success
*/
@JvmStatic
@Throws(IOException::class)
fun getCredentials(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, todaySeconds: Long): CredentialResponse {
val todayPlus7 = todaySeconds + 7.days.inWholeSeconds
val request = WebSocketRequestMessage.get("/v1/certificate/auth/group?redemptionStartSeconds=$todaySeconds&redemptionEndSeconds=$todayPlus7")
return NetworkResult.fromWebSocketRequest(authWebSocket, request, CredentialResponse::class).successOrThrow()
}
}

View File

@@ -11,6 +11,7 @@ import org.signal.svr2.proto.DeleteRequest
import org.signal.svr2.proto.ExposeRequest
import org.signal.svr2.proto.Request
import org.signal.svr2.proto.RestoreRequest
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.kbs.PinHashUtil
@@ -21,11 +22,13 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.InvalidReque
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.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.get
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.util.Hex
import org.whispersystems.signalservice.internal.util.JsonUtil
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.io.IOException
import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse
import org.signal.svr2.proto.ExposeResponse as ProtoExposeResponse
@@ -37,7 +40,7 @@ import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse
class SecureValueRecoveryV2(
private val serviceConfiguration: SignalServiceConfiguration,
private val mrEnclave: String,
private val pushServiceSocket: PushServiceSocket
private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket
) : SecureValueRecovery {
companion object {
@@ -92,7 +95,8 @@ class SecureValueRecoveryV2(
@Throws(IOException::class)
override fun authorization(): AuthCredentials {
return pushServiceSocket.svr2Authorization
val request = WebSocketRequestMessage.get("/v2/backup/auth")
return NetworkResult.fromWebSocketRequest(authWebSocket, request, AuthCredentials::class).successOrThrow()
}
override fun toString(): String {

View File

@@ -10,6 +10,7 @@ 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.net.Network
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
@@ -17,11 +18,13 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteRespon
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.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.get
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 org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.io.IOException
/**
@@ -29,7 +32,7 @@ import java.io.IOException
*/
class SecureValueRecoveryV3(
private val network: Network,
private val pushServiceSocket: PushServiceSocket
private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket
) : SecureValueRecovery {
companion object {
@@ -61,7 +64,7 @@ class SecureValueRecoveryV3(
override fun restoreDataPostRegistration(userPin: String): RestoreResponse {
val authorization: Svr3Credentials = try {
pushServiceSocket.svr3Authorization
svr3Authorization().successOrThrow()
} catch (e: NonSuccessfulResponseCodeException) {
return RestoreResponse.ApplicationError(e)
} catch (e: IOException) {
@@ -110,7 +113,12 @@ class SecureValueRecoveryV3(
@Throws(IOException::class)
override fun authorization(): AuthCredentials {
return pushServiceSocket.svr3Authorization.toAuthCredential()
return svr3Authorization().successOrThrow().toAuthCredential()
}
private fun svr3Authorization(): NetworkResult<Svr3Credentials> {
val request = WebSocketRequestMessage.get("/v3/backup/auth")
return NetworkResult.fromWebSocketRequest(authWebSocket, request, Svr3Credentials::class)
}
override fun toString(): String {

View File

@@ -43,7 +43,6 @@ import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.PreKeyCollection;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
@@ -51,7 +50,6 @@ import org.whispersystems.signalservice.api.messages.calls.CallingResponse;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -218,7 +216,6 @@ public class PushServiceSocket {
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
private static final String STICKER_PATH = "stickers/%s/full/%d";
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d";
private static final String GROUPSV2_GROUP = "/v2/groups/";
private static final String GROUPSV2_GROUP_PASSWORD = "/v2/groups/?inviteLinkPassword=%s";
private static final String GROUPSV2_GROUP_CHANGES = "/v2/groups/logs/%s?maxSupportedChangeEpoch=%d&includeFirstState=%s&includeLastState=false";
@@ -251,9 +248,6 @@ public class PushServiceSocket {
private static final String REGISTRATION_PATH = "/v1/registration";
private static final String SVR2_AUTH = "/v2/backup/auth";
private static final String SVR3_AUTH = "/v3/backup/auth";
private static final String BACKUP_AUTH_CHECK_V2 = "/v2/backup/auth/check";
private static final String BACKUP_AUTH_CHECK_V3 = "/v3/backup/auth/check";
@@ -416,18 +410,6 @@ public class PushServiceSocket {
return JsonUtil.fromJson(response, VerifyAccountResponse.class);
}
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 void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException {
String body = JsonUtil.toJson(request);
makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE);
@@ -2314,20 +2296,6 @@ public class PushServiceSocket {
}
}
public CredentialResponse retrieveGroupsV2Credentials(long todaySeconds)
throws IOException
{
long todayPlus7 = todaySeconds + TimeUnit.DAYS.toSeconds(7);
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, todaySeconds, todayPlus7),
"GET",
null,
NO_HEADERS,
NO_HANDLER,
SealedSenderAccess.NONE);
return JsonUtil.fromJson(response, CredentialResponse.class);
}
private static final ResponseCodeHandler GROUPS_V2_PUT_RESPONSE_HANDLER = (responseCode, body, getHeader) -> {
if (getHeader.apply("X-Signal-Timestamp") == null) {
throw new NonSuccessfulResponseCodeException(500, "Missing timestamp header");