Add two-phase commit support for SVR2.
This commit is contained in:
parent
a0c1b072b6
commit
15c248184f
7 changed files with 229 additions and 61 deletions
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
|
@ -212,5 +212,12 @@
|
||||||
</rules>
|
</rules>
|
||||||
</arrangement>
|
</arrangement>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
|
@ -185,7 +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_SVR2_URL", "\"https://svr2.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\"}"
|
||||||
|
@ -201,7 +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 "String", "SVR2_MRENCLAVE", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\""
|
||||||
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\")"
|
||||||
|
@ -378,6 +378,8 @@ android {
|
||||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||||
|
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
||||||
|
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
|
||||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||||
|
|
|
@ -3,9 +3,9 @@ package org.whispersystems.signalservice.api.svr
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.signal.libsignal.svr2.PinHash
|
|
||||||
import org.signal.svr2.proto.BackupRequest
|
import org.signal.svr2.proto.BackupRequest
|
||||||
import org.signal.svr2.proto.DeleteRequest
|
import org.signal.svr2.proto.DeleteRequest
|
||||||
|
import org.signal.svr2.proto.ExposeRequest
|
||||||
import org.signal.svr2.proto.Request
|
import org.signal.svr2.proto.Request
|
||||||
import org.signal.svr2.proto.RestoreRequest
|
import org.signal.svr2.proto.RestoreRequest
|
||||||
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException
|
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException
|
||||||
|
@ -13,9 +13,11 @@ import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
import org.whispersystems.signalservice.api.kbs.PinHashUtil
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||||
|
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse
|
import org.signal.svr2.proto.BackupResponse as ProtoBackupResponse
|
||||||
|
import org.signal.svr2.proto.ExposeResponse as ProtoExposeResponse
|
||||||
import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse
|
import org.signal.svr2.proto.RestoreResponse as ProtoRestoreResponse
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,45 +30,20 @@ class SecureValueRecoveryV2(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the provided data on the SVR service with the provided PIN.
|
* Begins a PIN change.
|
||||||
|
*
|
||||||
|
* Under the hood, setting a PIN is a two-phase process. This is abstracted through the [PinChangeSession].
|
||||||
|
* To use it, simply call [PinChangeSession.execute], which will return the result of the operation.
|
||||||
|
* If the operation is not successful and warrants a retry, it is extremely important to use the same [PinChangeSession].
|
||||||
|
*
|
||||||
|
* Do not have any automated retry system that calls [setPin] unconditionally. Always reuse the same [PinChangeSession]
|
||||||
|
* for as long as it is still valid (i.e. as long as you're still trying to set the same PIN).
|
||||||
*
|
*
|
||||||
* @param pin The user-specified PIN.
|
* @param pin The user-specified PIN.
|
||||||
* @param masterKey The data to set on SVR.
|
* @param masterKey The data to set on SVR.
|
||||||
*/
|
*/
|
||||||
fun setPin(pin: PinHash, masterKey: MasterKey): Single<BackupResponse> {
|
fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||||
val data = PinHashUtil.createNewKbsData(pin, masterKey)
|
return PinChangeSession(userPin, 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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,22 +53,22 @@ class SecureValueRecoveryV2(
|
||||||
*
|
*
|
||||||
* If the user is already registered, use [restoreDataPostRegistration]
|
* If the user is already registered, use [restoreDataPostRegistration]
|
||||||
*/
|
*/
|
||||||
fun restoreDataPreRegistration(authorization: String, pinHash: PinHash): Single<RestoreResponse> {
|
fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): Single<RestoreResponse> {
|
||||||
return restoreData(Single.just(authorization), pinHash)
|
return restoreData(Single.just(authorization), userPin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores data from SVR. Only intended to be called if the user is already registered. If the user is not yet registered, use [restoreDataPreRegistration]
|
* Restores data from SVR. Only intended to be called if the user is already registered. If the user is not yet registered, use [restoreDataPreRegistration]
|
||||||
*/
|
*/
|
||||||
fun restoreDataPostRegistration(pinHash: PinHash): Single<RestoreResponse> {
|
fun restoreDataPostRegistration(userPin: String): Single<RestoreResponse> {
|
||||||
return restoreData(getAuthorization(), pinHash)
|
return restoreData(getAuthorization(), userPin)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the user's SVR data from the service.
|
* Deletes the user's SVR data from the service.
|
||||||
*/
|
*/
|
||||||
fun deleteData(): Single<DeleteResponse> {
|
fun deleteData(): Single<DeleteResponse> {
|
||||||
val request = Request(delete = DeleteRequest())
|
val request: (Svr2PinHasher) -> Request = { Request(delete = DeleteRequest()) }
|
||||||
|
|
||||||
return getAuthorization()
|
return getAuthorization()
|
||||||
.flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) }
|
.flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) }
|
||||||
|
@ -106,18 +83,27 @@ class SecureValueRecoveryV2(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreData(authorization: Single<String>, pinHash: PinHash): Single<RestoreResponse> {
|
private fun restoreData(authorization: Single<AuthCredentials>, userPin: String): Single<RestoreResponse> {
|
||||||
val request = Request(
|
val normalizedPin: ByteArray = PinHashUtil.normalize(userPin)
|
||||||
restore = RestoreRequest(pin = pinHash.accessKey().toByteString())
|
|
||||||
)
|
|
||||||
|
|
||||||
return authorization
|
return authorization
|
||||||
.flatMap { auth -> Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth, request) }
|
.flatMap { auth ->
|
||||||
.map { response ->
|
Svr2Socket(serviceConfiguration, mrEnclave).makeRequest(auth) { pinHasher ->
|
||||||
|
val pinHash = pinHasher.hash(normalizedPin)
|
||||||
|
|
||||||
|
Request(
|
||||||
|
restore = RestoreRequest(
|
||||||
|
pin = pinHash.accessKey().toByteString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { (response, pinHasher) ->
|
||||||
when (response.restore?.status) {
|
when (response.restore?.status) {
|
||||||
ProtoRestoreResponse.Status.OK -> {
|
ProtoRestoreResponse.Status.OK -> {
|
||||||
val ciphertext: ByteArray = response.restore.data_.toByteArray()
|
val ciphertext: ByteArray = response.restore.data_.toByteArray()
|
||||||
try {
|
try {
|
||||||
|
val pinHash = pinHasher.hash(normalizedPin)
|
||||||
val masterKey: MasterKey = PinHashUtil.decryptKbsDataIVCipherText(pinHash, ciphertext).masterKey
|
val masterKey: MasterKey = PinHashUtil.decryptKbsDataIVCipherText(pinHash, ciphertext).masterKey
|
||||||
RestoreResponse.Success(masterKey)
|
RestoreResponse.Success(masterKey)
|
||||||
} catch (e: InvalidCiphertextException) {
|
} catch (e: InvalidCiphertextException) {
|
||||||
|
@ -148,15 +134,126 @@ class SecureValueRecoveryV2(
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAuthorization(): Single<String> {
|
private fun getAuthorization(): Single<AuthCredentials> {
|
||||||
return Single.fromCallable { pushServiceSocket.svr2Authorization }
|
return Single.fromCallable { pushServiceSocket.svr2Authorization }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for doing all the work necessary for changing a PIN.
|
||||||
|
*
|
||||||
|
* It's primary purpose is to serve as an abstraction over the fact that there are actually two separate requests that need to be made:
|
||||||
|
*
|
||||||
|
* (1) Create the backup data (which resets the guess count), and
|
||||||
|
* (2) Expose that data, making it eligible to be restored.
|
||||||
|
*
|
||||||
|
* The first should _never_ be retried after it completes successfully, and this class will help ensure that doesn't happen by doing the
|
||||||
|
* proper bookkeeping.
|
||||||
|
*/
|
||||||
|
inner class PinChangeSession(
|
||||||
|
val userPin: String,
|
||||||
|
val masterKey: MasterKey,
|
||||||
|
private var setupComplete: Boolean = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the PIN change operation. This is safe to call repeatedly if you get back a retryable error.
|
||||||
|
*/
|
||||||
|
fun execute(): Single<BackupResponse> {
|
||||||
|
val normalizedPin: ByteArray = PinHashUtil.normalize(userPin)
|
||||||
|
|
||||||
|
return getAuthorization()
|
||||||
|
.flatMap { auth ->
|
||||||
|
if (setupComplete) {
|
||||||
|
Single.just(auth to ProtoBackupResponse(status = ProtoBackupResponse.Status.OK))
|
||||||
|
} else {
|
||||||
|
getBackupResponse(auth, normalizedPin).map { auth to it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnSuccess { (_, response) ->
|
||||||
|
if (response.status == ProtoBackupResponse.Status.OK) {
|
||||||
|
setupComplete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMap { (auth, response) ->
|
||||||
|
when (response.status) {
|
||||||
|
ProtoBackupResponse.Status.OK -> {
|
||||||
|
getExposeResponse(auth, normalizedPin)
|
||||||
|
}
|
||||||
|
ProtoBackupResponse.Status.REQUEST_INVALID -> {
|
||||||
|
Single.just(BackupResponse.ApplicationError(InvalidRequestException("BackupResponse returned status code for REQUEST_INVALID")))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Single.just(BackupResponse.ApplicationError(IllegalStateException("Unknown status: ${response.status}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onErrorReturn { throwable ->
|
||||||
|
when (throwable) {
|
||||||
|
is NonSuccessfulResponseCodeException -> BackupResponse.ApplicationError(throwable)
|
||||||
|
is IOException -> BackupResponse.NetworkError(throwable)
|
||||||
|
else -> BackupResponse.ApplicationError(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBackupResponse(authorization: AuthCredentials, normalizedPin: ByteArray): Single<ProtoBackupResponse> {
|
||||||
|
val request: (Svr2PinHasher) -> Request = { pinHasher ->
|
||||||
|
val hashedPin = pinHasher.hash(normalizedPin)
|
||||||
|
val data = PinHashUtil.createNewKbsData(hashedPin, masterKey)
|
||||||
|
|
||||||
|
Request(
|
||||||
|
backup = BackupRequest(
|
||||||
|
pin = data.kbsAccessKey.toByteString(),
|
||||||
|
data_ = data.cipherText.toByteString(),
|
||||||
|
maxTries = 10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Svr2Socket(serviceConfiguration, mrEnclave)
|
||||||
|
.makeRequest(authorization, request)
|
||||||
|
.map { (response, _) -> response.backup ?: throw IllegalStateException("Backup response not set!") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExposeResponse(authorization: AuthCredentials, normalizedPin: ByteArray): Single<BackupResponse> {
|
||||||
|
val request: (Svr2PinHasher) -> Request = { pinHasher ->
|
||||||
|
val hashedPin = pinHasher.hash(normalizedPin)
|
||||||
|
val data = PinHashUtil.createNewKbsData(hashedPin, masterKey)
|
||||||
|
|
||||||
|
Request(
|
||||||
|
expose = ExposeRequest(
|
||||||
|
data_ = data.cipherText.toByteString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Svr2Socket(serviceConfiguration, mrEnclave)
|
||||||
|
.makeRequest(authorization, request)
|
||||||
|
.map { (response, _) ->
|
||||||
|
when (response.expose?.status) {
|
||||||
|
ProtoExposeResponse.Status.OK -> {
|
||||||
|
BackupResponse.Success
|
||||||
|
}
|
||||||
|
ProtoExposeResponse.Status.ERROR -> {
|
||||||
|
BackupResponse.ExposeFailure
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
BackupResponse.ApplicationError(IllegalStateException("Backup response not set!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Response for setting a PIN. */
|
/** Response for setting a PIN. */
|
||||||
sealed class BackupResponse {
|
sealed class BackupResponse {
|
||||||
/** Operation completed successfully. */
|
/** Operation completed successfully. */
|
||||||
object Success : BackupResponse()
|
object Success : BackupResponse()
|
||||||
|
|
||||||
|
/** The operation failed because the server was unable to expose the backup data we created. There is no further action that can be taken besides logging the error and treating it as a success. */
|
||||||
|
object ExposeFailure : BackupResponse()
|
||||||
|
|
||||||
/** There as a network error. Not a bad response, but rather interference or some other inability to make a network request. */
|
/** 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()
|
data class NetworkError(val exception: IOException) : BackupResponse()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.signalservice.api.svr
|
||||||
|
|
||||||
|
import org.signal.libsignal.svr2.PinHash
|
||||||
|
import org.signal.libsignal.svr2.Svr2Client
|
||||||
|
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the various dependencies needed to create a [PinHash] in SVR2 without having to expose them.
|
||||||
|
*/
|
||||||
|
internal class Svr2PinHasher(
|
||||||
|
private val authCredentials: AuthCredentials,
|
||||||
|
private val client: Svr2Client
|
||||||
|
) {
|
||||||
|
fun hash(normalizedPin: ByteArray): PinHash {
|
||||||
|
return client.hashPin(normalizedPin, authCredentials.username().toByteArray(Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ import io.reactivex.rxjava3.core.SingleEmitter
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.WebSocket
|
import okhttp3.WebSocket
|
||||||
import okhttp3.WebSocketListener
|
import okhttp3.WebSocketListener
|
||||||
import okio.ByteString
|
import okio.ByteString
|
||||||
|
@ -21,6 +20,7 @@ import org.whispersystems.signalservice.api.util.Tls12SocketFactory
|
||||||
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory
|
import org.whispersystems.signalservice.api.util.TlsProxySocketFactory
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
||||||
|
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager
|
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager
|
||||||
import org.whispersystems.signalservice.internal.util.Hex
|
import org.whispersystems.signalservice.internal.util.Hex
|
||||||
import org.whispersystems.signalservice.internal.util.Util
|
import org.whispersystems.signalservice.internal.util.Util
|
||||||
|
@ -33,6 +33,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.SSLSocketFactory
|
import javax.net.ssl.SSLSocketFactory
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
|
import okhttp3.Response as OkHttpResponse
|
||||||
import org.signal.svr2.proto.Request as Svr2Request
|
import org.signal.svr2.proto.Request as Svr2Request
|
||||||
import org.signal.svr2.proto.Response as Svr2Response
|
import org.signal.svr2.proto.Response as Svr2Response
|
||||||
|
|
||||||
|
@ -46,11 +47,11 @@ internal class Svr2Socket(
|
||||||
private val svr2Url: SignalSvr2Url = chooseUrl(configuration.signalSvr2Urls)
|
private val svr2Url: SignalSvr2Url = chooseUrl(configuration.signalSvr2Urls)
|
||||||
private val okhttp: OkHttpClient = buildOkHttpClient(configuration, svr2Url)
|
private val okhttp: OkHttpClient = buildOkHttpClient(configuration, svr2Url)
|
||||||
|
|
||||||
fun makeRequest(authorization: String, clientRequest: Svr2Request): Single<Svr2Response> {
|
fun makeRequest(authorization: AuthCredentials, clientRequest: (Svr2PinHasher) -> Svr2Request): Single<Response> {
|
||||||
return Single.create { emitter ->
|
return Single.create { emitter ->
|
||||||
val openRequest: Request.Builder = Request.Builder()
|
val openRequest: Request.Builder = Request.Builder()
|
||||||
.url("${svr2Url.url}/v1/$mrEnclave")
|
.url("${svr2Url.url}/v1/$mrEnclave")
|
||||||
.addHeader("Authorization", authorization)
|
.addHeader("Authorization", authorization.asBasic())
|
||||||
|
|
||||||
if (svr2Url.hostHeader.isPresent) {
|
if (svr2Url.hostHeader.isPresent) {
|
||||||
openRequest.addHeader("Host", svr2Url.hostHeader.get())
|
openRequest.addHeader("Host", svr2Url.hostHeader.get())
|
||||||
|
@ -60,6 +61,7 @@ internal class Svr2Socket(
|
||||||
val webSocket = okhttp.newWebSocket(
|
val webSocket = okhttp.newWebSocket(
|
||||||
openRequest.build(),
|
openRequest.build(),
|
||||||
SvrWebSocketListener(
|
SvrWebSocketListener(
|
||||||
|
authorization = authorization,
|
||||||
mrEnclave = mrEnclave,
|
mrEnclave = mrEnclave,
|
||||||
clientRequest = clientRequest,
|
clientRequest = clientRequest,
|
||||||
emitter = emitter
|
emitter = emitter
|
||||||
|
@ -71,15 +73,17 @@ internal class Svr2Socket(
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SvrWebSocketListener(
|
private class SvrWebSocketListener(
|
||||||
|
private val authorization: AuthCredentials,
|
||||||
private val mrEnclave: String,
|
private val mrEnclave: String,
|
||||||
private val clientRequest: Svr2Request,
|
private val clientRequest: (Svr2PinHasher) -> Svr2Request,
|
||||||
private val emitter: SingleEmitter<Svr2Response>
|
private val emitter: SingleEmitter<Response>
|
||||||
) : WebSocketListener() {
|
) : WebSocketListener() {
|
||||||
|
|
||||||
private val stage = AtomicReference(Stage.WAITING_TO_INITIALIZE)
|
private val stage = AtomicReference(Stage.WAITING_TO_INITIALIZE)
|
||||||
private lateinit var client: Svr2Client
|
private lateinit var client: Svr2Client
|
||||||
|
private lateinit var pinHasher: Svr2PinHasher
|
||||||
|
|
||||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
override fun onOpen(webSocket: WebSocket, response: OkHttpResponse) {
|
||||||
Log.d(TAG, "[onOpen]")
|
Log.d(TAG, "[onOpen]")
|
||||||
stage.set(Stage.WAITING_FOR_CONNECTION)
|
stage.set(Stage.WAITING_FOR_CONNECTION)
|
||||||
}
|
}
|
||||||
|
@ -94,6 +98,7 @@ internal class Svr2Socket(
|
||||||
|
|
||||||
Stage.WAITING_FOR_CONNECTION -> {
|
Stage.WAITING_FOR_CONNECTION -> {
|
||||||
client = Svr2Client.create(Hex.fromStringCondensed(mrEnclave), bytes.toByteArray(), Instant.now())
|
client = Svr2Client.create(Hex.fromStringCondensed(mrEnclave), bytes.toByteArray(), Instant.now())
|
||||||
|
pinHasher = Svr2PinHasher(authorization, client)
|
||||||
|
|
||||||
Log.d(TAG, "[onMessage] Sending initial handshake...")
|
Log.d(TAG, "[onMessage] Sending initial handshake...")
|
||||||
webSocket.send(client.initialRequest().toByteString())
|
webSocket.send(client.initialRequest().toByteString())
|
||||||
|
@ -104,7 +109,7 @@ internal class Svr2Socket(
|
||||||
client.completeHandshake(bytes.toByteArray())
|
client.completeHandshake(bytes.toByteArray())
|
||||||
Log.d(TAG, "[onMessage] Handshake read success. Sending request...")
|
Log.d(TAG, "[onMessage] Handshake read success. Sending request...")
|
||||||
|
|
||||||
val ciphertextBytes = client.establishedSend(clientRequest.encode())
|
val ciphertextBytes = client.establishedSend(clientRequest(pinHasher).encode())
|
||||||
webSocket.send(ciphertextBytes.toByteString())
|
webSocket.send(ciphertextBytes.toByteString())
|
||||||
|
|
||||||
Log.d(TAG, "[onMessage] Request sent.")
|
Log.d(TAG, "[onMessage] Request sent.")
|
||||||
|
@ -113,7 +118,12 @@ internal class Svr2Socket(
|
||||||
|
|
||||||
Stage.WAITING_FOR_RESPONSE -> {
|
Stage.WAITING_FOR_RESPONSE -> {
|
||||||
Log.d(TAG, "[onMessage] Received response for our request.")
|
Log.d(TAG, "[onMessage] Received response for our request.")
|
||||||
emitter.onSuccess(Svr2Response.ADAPTER.decode(client.establishedRecv(bytes.toByteArray())))
|
emitter.onSuccess(
|
||||||
|
Response(
|
||||||
|
response = Svr2Response.ADAPTER.decode(client.establishedRecv(bytes.toByteArray())),
|
||||||
|
pinHasher = pinHasher
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Stage.CLOSED -> {
|
Stage.CLOSED -> {
|
||||||
|
@ -155,7 +165,7 @@ internal class Svr2Socket(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
override fun onFailure(webSocket: WebSocket, t: Throwable, response: OkHttpResponse?) {
|
||||||
if (emitter.tryOnError(t)) {
|
if (emitter.tryOnError(t)) {
|
||||||
Log.w(TAG, "[onFailure] response? " + (response != null), t)
|
Log.w(TAG, "[onFailure] response? " + (response != null), t)
|
||||||
stage.set(Stage.FAILED)
|
stage.set(Stage.FAILED)
|
||||||
|
@ -173,6 +183,11 @@ internal class Svr2Socket(
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Response(
|
||||||
|
val response: Svr2Response,
|
||||||
|
val pinHasher: Svr2PinHasher
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Svr2Socket::class.java.simpleName
|
private val TAG = Svr2Socket::class.java.simpleName
|
||||||
|
|
||||||
|
|
|
@ -429,11 +429,11 @@ public class PushServiceSocket {
|
||||||
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSvr2Authorization() throws IOException {
|
public AuthCredentials getSvr2Authorization() throws IOException {
|
||||||
String body = makeServiceRequest(SVR2_AUTH, "GET", null);
|
String body = makeServiceRequest(SVR2_AUTH, "GET", null);
|
||||||
AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class);
|
AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class);
|
||||||
|
|
||||||
return credentials.asBasic();
|
return credentials;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest)
|
public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest)
|
||||||
|
|
|
@ -8,6 +8,7 @@ message Request {
|
||||||
|
|
||||||
oneof inner {
|
oneof inner {
|
||||||
BackupRequest backup = 2;
|
BackupRequest backup = 2;
|
||||||
|
ExposeRequest expose = 5;
|
||||||
RestoreRequest restore = 3;
|
RestoreRequest restore = 3;
|
||||||
DeleteRequest delete = 4;
|
DeleteRequest delete = 4;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +17,7 @@ message Request {
|
||||||
message Response {
|
message Response {
|
||||||
oneof inner {
|
oneof inner {
|
||||||
BackupResponse backup = 1;
|
BackupResponse backup = 1;
|
||||||
|
ExposeResponse expose = 4;
|
||||||
RestoreResponse restore = 2;
|
RestoreResponse restore = 2;
|
||||||
DeleteResponse delete = 3;
|
DeleteResponse delete = 3;
|
||||||
}
|
}
|
||||||
|
@ -72,3 +74,26 @@ message DeleteRequest {
|
||||||
|
|
||||||
message DeleteResponse {
|
message DeleteResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ExposeRequest {
|
||||||
|
bytes data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// expose
|
||||||
|
//
|
||||||
|
|
||||||
|
message ExposeResponse {
|
||||||
|
enum Status {
|
||||||
|
UNSET = 0; // never returned
|
||||||
|
OK = 1; // successfully restored, [data] will be set
|
||||||
|
|
||||||
|
// If this status comes back after a successful Backup() call,
|
||||||
|
// this should be cause for concern.
|
||||||
|
// It means that someone has either reset, deleted, or tried to brute-force
|
||||||
|
// the backup since it was created.
|
||||||
|
ERROR = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status status = 1;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue