mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Add pre-alpha support for SVR2.
This commit is contained in:
committed by
Nicholas Tinsley
parent
8cd0ac5451
commit
6cf4dbc78c
@@ -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\")"
|
||||
|
||||
@@ -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<Int, SignalServiceConfiguration> = 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<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 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(
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
74
libsignal/service/src/main/protowire/SVR2.proto
Normal file
74
libsignal/service/src/main/protowire/SVR2.proto
Normal 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 {
|
||||
}
|
||||
Reference in New Issue
Block a user