From 6cf4dbc78c41fd97cea31e88031cf903b6b3f7dd Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 23 Mar 2023 10:51:00 -0400 Subject: [PATCH] Add pre-alpha support for SVR2. --- app/build.gradle | 2 + .../push/SignalServiceNetworkAccess.kt | 60 ++--- .../api/SignalServiceAccountManager.java | 5 + .../api/services/CdsiSocket.java | 2 +- .../api/svr/SecureValueRecoveryV2.kt | 199 ++++++++++++++++ .../signalservice/api/svr/Svr2Socket.kt | 212 ++++++++++++++++++ .../SignalServiceConfiguration.java | 80 ------- .../SignalServiceConfiguration.kt | 21 ++ .../internal/configuration/SignalSvr2Url.kt | 14 ++ .../internal/push/NowhereBufferedSink.java | 2 + .../internal/push/PushServiceSocket.java | 8 + .../service/src/main/protowire/SVR2.proto | 74 ++++++ 12 files changed, 571 insertions(+), 108 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalSvr2Url.kt create mode 100644 libsignal/service/src/main/protowire/SVR2.proto diff --git a/app/build.gradle b/app/build.gradle index 2caeeebf9c..32e2e96c88 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -185,6 +185,7 @@ android { buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" + buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"" buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"" buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"" buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}" @@ -200,6 +201,7 @@ android { buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"" + buildConfigField "String", "SVR2_MRENCLAVE", "\"dc9fd472a5a9c871a3c7f76f1af60aa9c1f314abf2e8d1e0c4ba25c8aaa2848c\"" buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " + "\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " + "\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")" diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index 20911f43ac..c8fbb4504a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -26,6 +26,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl +import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url import java.io.IOException import java.util.Optional @@ -73,6 +74,7 @@ open class SignalServiceNetworkAccess(context: Context) { private const val F_CDN_HOST = "cdn.signal.org.global.prod.fastly.net" private const val F_CDN2_HOST = "cdn2.signal.org.global.prod.fastly.net" private const val F_CDSI_HOST = "cdsi-signal.global.ssl.fastly.net" + private const val F_SVR2_HOST = "svr2-signal.global.ssl.fastly.net" private const val F_KBS_HOST = "api.backup.signal.org.global.prod.fastly.net" private val GMAPS_CONNECTION_SPEC = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) @@ -159,18 +161,19 @@ open class SignalServiceNetworkAccess(context: Context) { private val fUrls = arrayOf("https://cdn.sstatic.net", "https://github.githubassets.com", "https://pinterest.com", "https://open.scdn.co", "https://www.redditstatic.com") private val fConfig: SignalServiceConfiguration = SignalServiceConfiguration( - fUrls.map { SignalServiceUrl(it, F_SERVICE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), - mapOf( + signalServiceUrls = fUrls.map { SignalServiceUrl(it, F_SERVICE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + signalCdnUrlMap = mapOf( 0 to fUrls.map { SignalCdnUrl(it, F_CDN_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), 2 to fUrls.map { SignalCdnUrl(it, F_CDN2_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray() ), - fUrls.map { SignalKeyBackupServiceUrl(it, F_KBS_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), - fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), - fUrls.map { SignalCdsiUrl(it, F_CDSI_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), - interceptors, - Optional.of(DNS), - Optional.empty(), - zkGroupServerPublicParams + signalKeyBackupServiceUrls = fUrls.map { SignalKeyBackupServiceUrl(it, F_KBS_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + signalStorageUrls = fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + signalCdsiUrls = fUrls.map { SignalCdsiUrl(it, F_CDSI_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + signalSvr2Urls = fUrls.map { SignalSvr2Url(it, fTrustStore, F_SVR2_HOST, APP_CONNECTION_SPEC) }.toTypedArray(), + networkInterceptors = interceptors, + dns = Optional.of(DNS), + signalProxy = Optional.empty(), + zkGroupServerPublicParams = zkGroupServerPublicParams ) private val censorshipConfiguration: Map = mapOf( @@ -209,18 +212,19 @@ open class SignalServiceNetworkAccess(context: Context) { ) open val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration( - arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)), - mapOf( + signalServiceUrls = arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)), + signalCdnUrlMap = mapOf( 0 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, serviceTrustStore)), 2 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, serviceTrustStore)) ), - arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)), - arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)), - arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)), - interceptors, - Optional.of(DNS), - if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(), - zkGroupServerPublicParams + signalKeyBackupServiceUrls = arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)), + signalStorageUrls = arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)), + signalCdsiUrls = arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)), + signalSvr2Urls = arrayOf(SignalSvr2Url(BuildConfig.SIGNAL_SVR2_URL, serviceTrustStore)), + networkInterceptors = interceptors, + dns = Optional.of(DNS), + signalProxy = if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(), + zkGroupServerPublicParams = zkGroupServerPublicParams ) open fun getConfiguration(): SignalServiceConfiguration { @@ -272,20 +276,22 @@ open class SignalServiceNetworkAccess(context: Context) { val kbsUrls: Array = hostConfigs.map { SignalKeyBackupServiceUrl("${it.baseUrl}/backup", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() val storageUrls: Array = hostConfigs.map { SignalStorageUrl("${it.baseUrl}/storage", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() val cdsiUrls: Array = hostConfigs.map { SignalCdsiUrl("${it.baseUrl}/cdsi", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val svr2Urls: Array = hostConfigs.map { SignalSvr2Url("${it.baseUrl}/svr2", gTrustStore, it.host, it.connectionSpec) }.toTypedArray() return SignalServiceConfiguration( - serviceUrls, - mapOf( + signalServiceUrls = serviceUrls, + signalCdnUrlMap = mapOf( 0 to cdnUrls, 2 to cdn2Urls ), - kbsUrls, - storageUrls, - cdsiUrls, - interceptors, - Optional.of(DNS), - Optional.empty(), - zkGroupServerPublicParams + signalKeyBackupServiceUrls = kbsUrls, + signalStorageUrls = storageUrls, + signalCdsiUrls = cdsiUrls, + signalSvr2Urls = arrayOf(), + networkInterceptors = interceptors, + dns = Optional.of(DNS), + signalProxy = Optional.empty(), + zkGroupServerPublicParams = zkGroupServerPublicParams ) } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 599c384f82..476a5c6311 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -49,6 +49,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; 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.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -173,6 +174,10 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getUuidOnlySenderCertificate(); } + public SecureValueRecoveryV2 getSecureValueRecoveryV2(String mrEnclave) { + return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket); + } + /** * V1 PINs are no longer used in favor of V2 PINs stored on KBS. * diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java index 1af1638165..37eb7f6403 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java @@ -165,7 +165,7 @@ final class CdsiSocket { webSocket.close(1000, "OK"); break; } - } catch (IOException | SgxCommunicationFailureException | AttestationDataException e) { + } catch (IOException | AttestationDataException | SgxCommunicationFailureException e) { Log.w(TAG, e); webSocket.close(1000, "OK"); emitter.tryOnError(e); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt new file mode 100644 index 0000000000..833955e414 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt @@ -0,0 +1,199 @@ +package org.whispersystems.signalservice.api.svr + +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import okio.ByteString.Companion.toByteString +import org.signal.libsignal.svr2.PinHash +import org.signal.svr2.proto.BackupRequest +import org.signal.svr2.proto.DeleteRequest +import org.signal.svr2.proto.Request +import org.signal.svr2.proto.RestoreRequest +import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.kbs.PinHashUtil +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import java.io.IOException +import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse +import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse + +/** + * An interface for working with V2 of the Secure Value Recovery service. + */ +class SecureValueRecoveryV2( + private val serviceConfiguration: SignalServiceConfiguration, + private val mrEnclave: String, + private val pushServiceSocket: PushServiceSocket +) { + + /** + * Sets the provided data on the SVR service with the provided PIN. + * + * @param pin The user-specified PIN. + * @param masterKey The data to set on SVR. + */ + fun setPin(pin: PinHash, masterKey: MasterKey): Single { + val data = PinHashUtil.createNewKbsData(pin, masterKey) + + val request = Request( + backup = BackupRequest( + pin = data.kbsAccessKey.toByteString(), + data_ = data.cipherText.toByteString(), + maxTries = 10 + ) + ) + + return getAuthorization() + .flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) } + .map { response -> + when (response.backup?.status) { + ProtoBackupResponse.Status.OK -> { + BackupResponse.Success + } + ProtoBackupResponse.Status.REQUEST_INVALID -> { + BackupResponse.ApplicationError(InvalidRequestException("BackupResponse returned status code for REQUEST_INVALID")) + } + else -> { + BackupResponse.ApplicationError(IllegalStateException("Unknown status: ${response.backup?.status}")) + } + } + } + .onErrorReturn { throwable -> + when (throwable) { + is NonSuccessfulResponseCodeException -> BackupResponse.ApplicationError(throwable) + is IOException -> BackupResponse.NetworkError(throwable) + else -> BackupResponse.ApplicationError(throwable) + } + } + .subscribeOn(Schedulers.io()) + } + + /** + * Restores the user's SVR data from the service. Intended to be called in the situation where the user is not yet registered. + * 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] + */ + fun restoreDataPreRegistration(authorization: String, pinHash: PinHash): Single { + return restoreData(Single.just(authorization), pinHash) + } + + /** + * 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(pinHash: PinHash): Single { + return restoreData(getAuthorization(), pinHash) + } + + /** + * Deletes the user's SVR data from the service. + */ + fun deleteData(): Single { + val request = Request(delete = DeleteRequest()) + + return getAuthorization() + .flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) } + .map { DeleteResponse.Success as DeleteResponse } + .onErrorReturn { throwable -> + when (throwable) { + is NonSuccessfulResponseCodeException -> DeleteResponse.ApplicationError(throwable) + is IOException -> DeleteResponse.NetworkError(throwable) + else -> DeleteResponse.ApplicationError(throwable) + } + } + .subscribeOn(Schedulers.io()) + } + + private fun restoreData(authorization: Single, pinHash: PinHash): Single { + val request = Request( + restore = RestoreRequest(pin = pinHash.accessKey().toByteString()) + ) + + return authorization + .flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) } + .map { response -> + when (response.restore?.status) { + ProtoRestoreResponse.Status.OK -> { + val ciphertext: ByteArray = response.restore.data_.toByteArray() + try { + val masterKey: MasterKey = PinHashUtil.decryptKbsDataIVCipherText(pinHash, ciphertext).masterKey + RestoreResponse.Success(masterKey) + } catch (e: InvalidCiphertextException) { + RestoreResponse.ApplicationError(e) + } + } + ProtoRestoreResponse.Status.MISSING -> { + RestoreResponse.Missing + } + ProtoRestoreResponse.Status.PIN_MISMATCH -> { + RestoreResponse.PinMismatch(response.restore.tries) + } + ProtoRestoreResponse.Status.REQUEST_INVALID -> { + RestoreResponse.ApplicationError(InvalidRequestException("RestoreResponse returned status code for REQUEST_INVALID")) + } + else -> { + RestoreResponse.ApplicationError(IllegalStateException("Unknown status: ${response.backup?.status}")) + } + } + } + .onErrorReturn { throwable -> + when (throwable) { + is NonSuccessfulResponseCodeException -> RestoreResponse.ApplicationError(throwable) + is IOException -> RestoreResponse.NetworkError(throwable) + else -> RestoreResponse.ApplicationError(throwable) + } + } + .subscribeOn(Schedulers.io()) + } + + private fun getAuthorization(): Single { + return Single.fromCallable { pushServiceSocket.svr2Authorization } + } + + /** Response for setting a PIN. */ + sealed class BackupResponse { + /** Operation completed successfully. */ + object Success : BackupResponse() + + /** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */ + data class NetworkError(val exception: IOException) : BackupResponse() + + /** Something went wrong when making the request that is related to application logic. */ + data class ApplicationError(val exception: Throwable) : BackupResponse() + } + + /** Response for restoring data with you PIN. */ + sealed class RestoreResponse { + /** Operation completed successfully. Includes the restored data. */ + data class Success(val masterKey: MasterKey) : RestoreResponse() + + /** No data was found for this user. Could mean that none ever existed, or that the service deleted the data after too many incorrect PIN guesses. */ + object Missing : RestoreResponse() + + /** The PIN was incorrect. Includes the number of attempts the user has remaining. */ + data class PinMismatch(val triesRemaining: Int) : RestoreResponse() + + /** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */ + data class NetworkError(val exception: IOException) : RestoreResponse() + + /** Something went wrong when making the request that is related to application logic. */ + data class ApplicationError(val exception: Throwable) : RestoreResponse() + } + + /** Response for deleting data. */ + sealed class DeleteResponse { + /** Operation completed successfully. */ + object Success : DeleteResponse() + + /** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */ + data class NetworkError(val exception: IOException) : DeleteResponse() + + /** Something went wrong when making the request that is related to application logic. */ + data class ApplicationError(val exception: Throwable) : DeleteResponse() + } + + /** Exception indicating that we received a response from the service that our request was invalid. */ + class InvalidRequestException(message: String) : Exception(message) +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt new file mode 100644 index 0000000000..78f7a8966c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/svr/Svr2Socket.kt @@ -0,0 +1,212 @@ +package org.whispersystems.signalservice.api.svr + +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.core.SingleEmitter +import okhttp3.ConnectionSpec +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import okio.ByteString.Companion.toByteString +import org.signal.libsignal.attest.AttestationDataException +import org.signal.libsignal.protocol.logging.Log +import org.signal.libsignal.protocol.util.Pair +import org.signal.libsignal.sgxsession.SgxCommunicationFailureException +import org.signal.libsignal.svr2.Svr2Client +import org.whispersystems.signalservice.api.push.TrustStore +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.util.Tls12SocketFactory +import org.whispersystems.signalservice.api.util.TlsProxySocketFactory +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration +import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url +import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager +import org.whispersystems.signalservice.internal.util.Hex +import org.whispersystems.signalservice.internal.util.Util +import java.io.IOException +import java.security.KeyManagementException +import java.security.NoSuchAlgorithmException +import java.time.Instant +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager +import org.signal.svr2.proto.Request as Svr2Request +import org.signal.svr2.proto.Response as Svr2Response + +/** + * Handles the websocket and general lifecycle of an SVR2 request. + */ +internal class Svr2Socket( + configuration: SignalServiceConfiguration, + private val mrEnclave: String +) { + private val svr2Url: SignalSvr2Url = chooseUrl(configuration.signalSvr2Urls) + private val okhttp: OkHttpClient = buildOkHttpClient(configuration, svr2Url) + + fun makeRequest(authorization: String, clientRequest: Svr2Request): Single { + return Single.create { emitter -> + val openRequest: Request.Builder = Request.Builder() + .url("${svr2Url.url}/v1/$mrEnclave") + .addHeader("Authorization", authorization) + + if (svr2Url.hostHeader.isPresent) { + openRequest.addHeader("Host", svr2Url.hostHeader.get()) + Log.w(TAG, "Using alternate host: ${svr2Url.hostHeader.get()}") + } + + val webSocket = okhttp.newWebSocket( + openRequest.build(), + SvrWebSocketListener( + mrEnclave = mrEnclave, + clientRequest = clientRequest, + emitter = emitter + ) + ) + + emitter.setCancellable { webSocket.close(1000, "OK") } + } + } + + private class SvrWebSocketListener( + private val mrEnclave: String, + private val clientRequest: Svr2Request, + private val emitter: SingleEmitter + ) : WebSocketListener() { + + private val stage = AtomicReference(Stage.WAITING_TO_INITIALIZE) + private lateinit var client: Svr2Client + + override fun onOpen(webSocket: WebSocket, response: Response) { + Log.d(TAG, "[onOpen]") + stage.set(Stage.WAITING_FOR_CONNECTION) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + Log.d(TAG, "[onMessage] stage: " + stage.get()) + try { + when (stage.get()!!) { + Stage.WAITING_TO_INITIALIZE -> { + throw IOException("Received a message before we were open!") + } + + Stage.WAITING_FOR_CONNECTION -> { + client = Svr2Client.create_NOT_FOR_PRODUCTION(Hex.fromStringCondensed(mrEnclave), bytes.toByteArray(), Instant.now()) + + Log.d(TAG, "[onMessage] Sending initial handshake...") + webSocket.send(client.initialRequest().toByteString()) + stage.set(Stage.WAITING_FOR_HANDSHAKE) + } + + Stage.WAITING_FOR_HANDSHAKE -> { + client.completeHandshake(bytes.toByteArray()) + Log.d(TAG, "[onMessage] Handshake read success. Sending request...") + + val ciphertextBytes = client.establishedSend(clientRequest.encode()) + webSocket.send(ciphertextBytes.toByteString()) + + Log.d(TAG, "[onMessage] Request sent.") + stage.set(Stage.WAITING_FOR_RESPONSE) + } + + Stage.WAITING_FOR_RESPONSE -> { + Log.d(TAG, "[onMessage] Received response for our request.") + emitter.onSuccess(Svr2Response.ADAPTER.decode(client.establishedRecv(bytes.toByteArray()))) + } + + Stage.CLOSED -> { + Log.w(TAG, "[onMessage] Received a message after the websocket closed! Ignoring.") + } + + Stage.FAILED -> { + Log.w(TAG, "[onMessage] Received a message after we entered the failure state! Ignoring.") + webSocket.close(1000, "OK") + } + } + } catch (e: IOException) { + Log.w(TAG, e) + webSocket.close(1000, "OK") + emitter.tryOnError(e) + } catch (e: AttestationDataException) { + Log.w(TAG, e) + webSocket.close(1000, "OK") + emitter.tryOnError(e) + } catch (e: SgxCommunicationFailureException) { + Log.w(TAG, e) + webSocket.close(1000, "OK") + emitter.tryOnError(e) + } + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + Log.i(TAG, "[onClosing] code: $code, reason: $reason") + + if (code == 1000) { + emitter.tryOnError(IOException("Websocket was closed with code 1000")) + stage.set(Stage.CLOSED) + } else { + Log.w(TAG, "Remote side is closing with non-normal code $code") + webSocket.close(1000, "Remote closed with code $code") + stage.set(Stage.FAILED) + + emitter.tryOnError(NonSuccessfulResponseCodeException(code)) + } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + if (emitter.tryOnError(t)) { + Log.w(TAG, "[onFailure] response? " + (response != null), t) + stage.set(Stage.FAILED) + webSocket.close(1000, "OK") + } + } + } + + private enum class Stage { + WAITING_TO_INITIALIZE, + WAITING_FOR_CONNECTION, + WAITING_FOR_HANDSHAKE, + WAITING_FOR_RESPONSE, + CLOSED, + FAILED + } + + companion object { + private val TAG = Svr2Socket::class.java.simpleName + + private fun buildOkHttpClient(configuration: SignalServiceConfiguration, svr2Url: SignalSvr2Url): OkHttpClient { + val socketFactory = createTlsSocketFactory(svr2Url.trustStore) + val builder = OkHttpClient.Builder().sslSocketFactory(Tls12SocketFactory(socketFactory.first()), socketFactory.second()).connectionSpecs(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)).retryOnConnectionFailure(false).readTimeout(30, TimeUnit.SECONDS).connectTimeout(30, TimeUnit.SECONDS) + + for (interceptor in configuration.networkInterceptors) { + builder.addInterceptor(interceptor) + } + + if (configuration.signalProxy.isPresent) { + val proxy = configuration.signalProxy.get() + builder.socketFactory(TlsProxySocketFactory(proxy.host, proxy.port, configuration.dns)) + } + + return builder.build() + } + + private fun createTlsSocketFactory(trustStore: TrustStore): Pair { + return try { + val context = SSLContext.getInstance("TLS") + val trustManagers = BlacklistingTrustManager.createFor(trustStore) + context.init(null, trustManagers, null) + Pair(context.socketFactory, trustManagers[0] as X509TrustManager) + } catch (e: NoSuchAlgorithmException) { + throw AssertionError(e) + } catch (e: KeyManagementException) { + throw AssertionError(e) + } + } + + private fun chooseUrl(urls: Array): SignalSvr2Url { + return urls[(Math.random() * urls.size).toInt()] + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java deleted file mode 100644 index ee3d5f71c2..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.whispersystems.signalservice.internal.configuration; - - - -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import okhttp3.Dns; -import okhttp3.Interceptor; - -public final class SignalServiceConfiguration { - - private final SignalServiceUrl[] signalServiceUrls; - private final Map signalCdnUrlMap; - private final SignalCdsiUrl[] signalCdsiUrls; - private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls; - private final SignalStorageUrl[] signalStorageUrls; - private final List networkInterceptors; - private final Optional dns; - private final Optional proxy; - private final byte[] zkGroupServerPublicParams; - - public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls, - Map signalCdnUrlMap, - SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls, - SignalStorageUrl[] signalStorageUrls, - SignalCdsiUrl[] signalCdsiUrls, - List networkInterceptors, - Optional dns, - Optional proxy, - byte[] zkGroupServerPublicParams) - { - this.signalServiceUrls = signalServiceUrls; - this.signalCdnUrlMap = signalCdnUrlMap; - this.signalCdsiUrls = signalCdsiUrls; - this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls; - this.signalStorageUrls = signalStorageUrls; - this.networkInterceptors = networkInterceptors; - this.dns = dns; - this.proxy = proxy; - this.zkGroupServerPublicParams = zkGroupServerPublicParams; - } - - public SignalServiceUrl[] getSignalServiceUrls() { - return signalServiceUrls; - } - - public Map getSignalCdnUrlMap() { - return signalCdnUrlMap; - } - - public SignalCdsiUrl[] getSignalCdsiUrls() { - return signalCdsiUrls; - } - - public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() { - return signalKeyBackupServiceUrls; - } - - public SignalStorageUrl[] getSignalStorageUrls() { - return signalStorageUrls; - } - - public List getNetworkInterceptors() { - return networkInterceptors; - } - - public Optional getDns() { - return dns; - } - - public byte[] getZkGroupServerPublicParams() { - return zkGroupServerPublicParams; - } - - public Optional getSignalProxy() { - return proxy; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt new file mode 100644 index 0000000000..2349b99eda --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalServiceConfiguration.kt @@ -0,0 +1,21 @@ +package org.whispersystems.signalservice.internal.configuration + +import okhttp3.Dns +import okhttp3.Interceptor +import java.util.Optional + +/** + * Defines all network configuration needed to connect to the Signal service. + */ +class SignalServiceConfiguration( + val signalServiceUrls: Array, + val signalCdnUrlMap: Map>, + val signalKeyBackupServiceUrls: Array, + val signalStorageUrls: Array, + val signalCdsiUrls: Array, + val signalSvr2Urls: Array, + val networkInterceptors: List, + val dns: Optional, + val signalProxy: Optional, + val zkGroupServerPublicParams: ByteArray +) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalSvr2Url.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalSvr2Url.kt new file mode 100644 index 0000000000..504a24994e --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/configuration/SignalSvr2Url.kt @@ -0,0 +1,14 @@ +package org.whispersystems.signalservice.internal.configuration + +import okhttp3.ConnectionSpec +import org.whispersystems.signalservice.api.push.TrustStore + +/** + * Configuration for reach the SVR2 service. + */ +class SignalSvr2Url( + url: String, + trustStore: TrustStore, + hostHeader: String? = null, + connectionSpec: ConnectionSpec? = null +) : SignalUrl(url, hostHeader, trustStore, connectionSpec) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/NowhereBufferedSink.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/NowhereBufferedSink.java index b7ec3b7c5e..4fd4bdd0e2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/NowhereBufferedSink.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/NowhereBufferedSink.java @@ -1,5 +1,7 @@ package org.whispersystems.signalservice.internal.push; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 8c58a4bb0b..f0327cbece 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -283,6 +283,7 @@ public class PushServiceSocket { private static final String REGISTRATION_PATH = "/v1/registration"; private static final String CDSI_AUTH = "/v2/directory/auth"; + private static final String SVR2_AUTH = "/v2/backup/auth"; private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; @@ -425,6 +426,13 @@ public class PushServiceSocket { return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); } + public String getSvr2Authorization() throws IOException { + String body = makeServiceRequest(SVR2_AUTH, "GET", null); + AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class); + + return credentials.asBasic(); + } + public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) throws IOException { diff --git a/libsignal/service/src/main/protowire/SVR2.proto b/libsignal/service/src/main/protowire/SVR2.proto new file mode 100644 index 0000000000..3d35260453 --- /dev/null +++ b/libsignal/service/src/main/protowire/SVR2.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.signal.svr2.proto"; + +message Request { + reserved 1; // backupId, only used by server + + oneof inner { + BackupRequest backup = 2; + RestoreRequest restore = 3; + DeleteRequest delete = 4; + } +} + +message Response { + oneof inner { + BackupResponse backup = 1; + RestoreResponse restore = 2; + DeleteResponse delete = 3; + } +} + +// +// backup +// + +message BackupRequest { + bytes data = 1; // between 16 and 48 bytes + bytes pin = 2; // 32 bytes + uint32 maxTries = 3; // in range [1,255] +} + +message BackupResponse { + enum Status { + UNSET = 0; // never returned + OK = 1; // successfully set db[backup_id]=data + REQUEST_INVALID = 2; // the request was not correctly specified + } + + Status status = 1; +} + +// +// restore +// + +message RestoreRequest { + bytes pin = 1; // 32 bytes +} + +message RestoreResponse { + enum Status { + UNSET = 0; // never returned + OK = 1; // successfully restored, [data] will be set + MISSING = 2; // db[backup_id] does not exist + PIN_MISMATCH = 3; // pin did not match, tries were decremented + REQUEST_INVALID = 4; // the request was not correctly specified, tries were not decremented + } + + Status status = 1; + bytes data = 2; // between 16 and 48 bytes, if set + uint32 tries = 3; // in range [0,255] +} + +// +// delete +// + +message DeleteRequest { +} + +message DeleteResponse { +} \ No newline at end of file