Add pre-alpha support for SVR2.

This commit is contained in:
Greyson Parrelli
2023-03-23 10:51:00 -04:00
committed by Nicholas Tinsley
parent 8cd0ac5451
commit 6cf4dbc78c
12 changed files with 571 additions and 108 deletions

View File

@@ -185,6 +185,7 @@ android {
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"" buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.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_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_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.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\"}" 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_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"" buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"dc9fd472a5a9c871a3c7f76f1af60aa9c1f314abf2e8d1e0c4ba25c8aaa2848c\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " + "\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")" "\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"

View File

@@ -26,6 +26,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
import java.io.IOException import java.io.IOException
import java.util.Optional 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_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_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_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 const val F_KBS_HOST = "api.backup.signal.org.global.prod.fastly.net"
private val GMAPS_CONNECTION_SPEC = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) 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 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( private val fConfig: SignalServiceConfiguration = SignalServiceConfiguration(
fUrls.map { SignalServiceUrl(it, F_SERVICE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), signalServiceUrls = fUrls.map { SignalServiceUrl(it, F_SERVICE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
mapOf( signalCdnUrlMap = mapOf(
0 to fUrls.map { SignalCdnUrl(it, F_CDN_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), 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() 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(), signalKeyBackupServiceUrls = fUrls.map { SignalKeyBackupServiceUrl(it, F_KBS_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), signalStorageUrls = fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
fUrls.map { SignalCdsiUrl(it, F_CDSI_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), signalCdsiUrls = fUrls.map { SignalCdsiUrl(it, F_CDSI_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(),
interceptors, signalSvr2Urls = fUrls.map { SignalSvr2Url(it, fTrustStore, F_SVR2_HOST, APP_CONNECTION_SPEC) }.toTypedArray(),
Optional.of(DNS), networkInterceptors = interceptors,
Optional.empty(), dns = Optional.of(DNS),
zkGroupServerPublicParams signalProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams
) )
private val censorshipConfiguration: Map<Int, SignalServiceConfiguration> = mapOf( private val censorshipConfiguration: Map<Int, SignalServiceConfiguration> = mapOf(
@@ -209,18 +212,19 @@ open class SignalServiceNetworkAccess(context: Context) {
) )
open val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration( open val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration(
arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)), signalServiceUrls = arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)),
mapOf( signalCdnUrlMap = mapOf(
0 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, serviceTrustStore)), 0 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, serviceTrustStore)),
2 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, serviceTrustStore)) 2 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, serviceTrustStore))
), ),
arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)), signalKeyBackupServiceUrls = arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)),
arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)), signalStorageUrls = arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)),
arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)), signalCdsiUrls = arrayOf(SignalCdsiUrl(BuildConfig.SIGNAL_CDSI_URL, serviceTrustStore)),
interceptors, signalSvr2Urls = arrayOf(SignalSvr2Url(BuildConfig.SIGNAL_SVR2_URL, serviceTrustStore)),
Optional.of(DNS), networkInterceptors = interceptors,
if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(), dns = Optional.of(DNS),
zkGroupServerPublicParams signalProxy = if (SignalStore.proxy().isProxyEnabled) Optional.ofNullable(SignalStore.proxy().proxy) else Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams
) )
open fun getConfiguration(): SignalServiceConfiguration { open fun getConfiguration(): SignalServiceConfiguration {
@@ -272,20 +276,22 @@ open class SignalServiceNetworkAccess(context: Context) {
val kbsUrls: Array<SignalKeyBackupServiceUrl> = hostConfigs.map { SignalKeyBackupServiceUrl("${it.baseUrl}/backup", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() val kbsUrls: Array<SignalKeyBackupServiceUrl> = hostConfigs.map { SignalKeyBackupServiceUrl("${it.baseUrl}/backup", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
val storageUrls: Array<SignalStorageUrl> = hostConfigs.map { SignalStorageUrl("${it.baseUrl}/storage", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() val storageUrls: Array<SignalStorageUrl> = hostConfigs.map { SignalStorageUrl("${it.baseUrl}/storage", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
val cdsiUrls: Array<SignalCdsiUrl> = hostConfigs.map { SignalCdsiUrl("${it.baseUrl}/cdsi", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() val cdsiUrls: Array<SignalCdsiUrl> = hostConfigs.map { SignalCdsiUrl("${it.baseUrl}/cdsi", it.host, gTrustStore, it.connectionSpec) }.toTypedArray()
val svr2Urls: Array<SignalSvr2Url> = hostConfigs.map { SignalSvr2Url("${it.baseUrl}/svr2", gTrustStore, it.host, it.connectionSpec) }.toTypedArray()
return SignalServiceConfiguration( return SignalServiceConfiguration(
serviceUrls, signalServiceUrls = serviceUrls,
mapOf( signalCdnUrlMap = mapOf(
0 to cdnUrls, 0 to cdnUrls,
2 to cdn2Urls 2 to cdn2Urls
), ),
kbsUrls, signalKeyBackupServiceUrls = kbsUrls,
storageUrls, signalStorageUrls = storageUrls,
cdsiUrls, signalCdsiUrls = cdsiUrls,
interceptors, signalSvr2Urls = arrayOf(),
Optional.of(DNS), networkInterceptors = interceptors,
Optional.empty(), dns = Optional.of(DNS),
zkGroupServerPublicParams signalProxy = Optional.empty(),
zkGroupServerPublicParams = zkGroupServerPublicParams
) )
} }

View File

@@ -49,6 +49,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.storage.StorageManifestKey; 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.CredentialsProvider;
import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.Preconditions;
import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.ServiceResponse;
@@ -173,6 +174,10 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.getUuidOnlySenderCertificate(); 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. * V1 PINs are no longer used in favor of V2 PINs stored on KBS.
* *

View File

@@ -165,7 +165,7 @@ final class CdsiSocket {
webSocket.close(1000, "OK"); webSocket.close(1000, "OK");
break; break;
} }
} catch (IOException | SgxCommunicationFailureException | AttestationDataException e) { } catch (IOException | AttestationDataException | SgxCommunicationFailureException e) {
Log.w(TAG, e); Log.w(TAG, e);
webSocket.close(1000, "OK"); webSocket.close(1000, "OK");
emitter.tryOnError(e); emitter.tryOnError(e);

View File

@@ -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<BackupResponse> {
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<RestoreResponse> {
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<RestoreResponse> {
return restoreData(getAuthorization(), pinHash)
}
/**
* Deletes the user's SVR data from the service.
*/
fun deleteData(): Single<DeleteResponse> {
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<String>, pinHash: PinHash): Single<RestoreResponse> {
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<String> {
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)
}

View File

@@ -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<Svr2Response> {
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<Svr2Response>
) : 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<SSLSocketFactory, X509TrustManager> {
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>): SignalSvr2Url {
return urls[(Math.random() * urls.size).toInt()]
}
}
}

View File

@@ -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<Integer, SignalCdnUrl[]> signalCdnUrlMap;
private final SignalCdsiUrl[] signalCdsiUrls;
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
private final SignalStorageUrl[] signalStorageUrls;
private final List<Interceptor> networkInterceptors;
private final Optional<Dns> dns;
private final Optional<SignalProxy> proxy;
private final byte[] zkGroupServerPublicParams;
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
Map<Integer, SignalCdnUrl[]> signalCdnUrlMap,
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
SignalStorageUrl[] signalStorageUrls,
SignalCdsiUrl[] signalCdsiUrls,
List<Interceptor> networkInterceptors,
Optional<Dns> dns,
Optional<SignalProxy> 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<Integer, SignalCdnUrl[]> getSignalCdnUrlMap() {
return signalCdnUrlMap;
}
public SignalCdsiUrl[] getSignalCdsiUrls() {
return signalCdsiUrls;
}
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
return signalKeyBackupServiceUrls;
}
public SignalStorageUrl[] getSignalStorageUrls() {
return signalStorageUrls;
}
public List<Interceptor> getNetworkInterceptors() {
return networkInterceptors;
}
public Optional<Dns> getDns() {
return dns;
}
public byte[] getZkGroupServerPublicParams() {
return zkGroupServerPublicParams;
}
public Optional<SignalProxy> getSignalProxy() {
return proxy;
}
}

View File

@@ -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<SignalServiceUrl>,
val signalCdnUrlMap: Map<Int, Array<SignalCdnUrl>>,
val signalKeyBackupServiceUrls: Array<SignalKeyBackupServiceUrl>,
val signalStorageUrls: Array<SignalStorageUrl>,
val signalCdsiUrls: Array<SignalCdsiUrl>,
val signalSvr2Urls: Array<SignalSvr2Url>,
val networkInterceptors: List<Interceptor>,
val dns: Optional<Dns>,
val signalProxy: Optional<SignalProxy>,
val zkGroupServerPublicParams: ByteArray
)

View File

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

View File

@@ -1,5 +1,7 @@
package org.whispersystems.signalservice.internal.push; package org.whispersystems.signalservice.internal.push;
import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;

View File

@@ -283,6 +283,7 @@ public class PushServiceSocket {
private static final String REGISTRATION_PATH = "/v1/registration"; private static final String REGISTRATION_PATH = "/v1/registration";
private static final String CDSI_AUTH = "/v2/directory/auth"; 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"; private static final String REPORT_SPAM = "/v1/messages/report/%s/%s";
@@ -425,6 +426,13 @@ public class PushServiceSocket {
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); 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) public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest)
throws IOException throws IOException
{ {

View File

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