Add common interface over SVR implementations.
This commit is contained in:
parent
51222738df
commit
38f2b39ac4
10 changed files with 531 additions and 59 deletions
|
@ -136,6 +136,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
|||
}
|
||||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from("SVR Playground"),
|
||||
summary = DSLSettingsText.from("Quickly test various SVR options and error conditions."),
|
||||
onClick = {
|
||||
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSvrPlaygroundFragment())
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("'Internal Details' button"),
|
||||
summary = DSLSettingsText.from("Show a button in conversation settings that lets you see more information about a user."),
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.svr
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.key.Key.Companion.Tab
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.viewModels
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
class InternalSvrPlaygroundFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: InternalSvrPlaygroundViewModel by viewModels()
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
val state: InternalSvrPlaygroundState by viewModel.state
|
||||
|
||||
SvrPlaygroundScreen(
|
||||
state = state,
|
||||
onTabSelected = viewModel::onTabSelected,
|
||||
onCreateClicked = viewModel::onCreateClicked,
|
||||
onRestoreClicked = viewModel::onRestoreClicked,
|
||||
onDeleteClicked = viewModel::onDeleteClicked,
|
||||
onPinChanged = viewModel::onPinChanged
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SvrPlaygroundScreen(
|
||||
state: InternalSvrPlaygroundState,
|
||||
modifier: Modifier = Modifier,
|
||||
onTabSelected: (SvrImplementation) -> Unit = {},
|
||||
onCreateClicked: () -> Unit = {},
|
||||
onRestoreClicked: () -> Unit = {},
|
||||
onDeleteClicked: () -> Unit = {},
|
||||
onPinChanged: (String) -> Unit = {}
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
TabRow(selectedTabIndex = state.options.indexOf(state.selected)) {
|
||||
state.options.forEach { option ->
|
||||
Tab(
|
||||
text = { Text(option.title) },
|
||||
selected = option == state.selected,
|
||||
onClick = { onTabSelected(option) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Create backup data",
|
||||
onClick = onCreateClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Restore backup data",
|
||||
onClick = onRestoreClicked
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = "Delete backup data",
|
||||
onClick = onDeleteClicked
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Text(text = "PIN: ")
|
||||
}
|
||||
Column {
|
||||
TextField(
|
||||
value = state.userPin,
|
||||
onValueChange = onPinChanged,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.loading) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(48.dp)) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(),
|
||||
color = Color.Blue
|
||||
)
|
||||
}
|
||||
} else if (state.lastResult != null) {
|
||||
Rows.TextRow(text = state.lastResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SvrPlaygroundScreenLightTheme() {
|
||||
SignalTheme(isDarkMode = false) {
|
||||
Surface {
|
||||
SvrPlaygroundScreen(
|
||||
state = InternalSvrPlaygroundState(
|
||||
options = persistentListOf(SvrImplementation.SVR1, SvrImplementation.SVR2)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SvrPlaygroundScreenDarkTheme() {
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Surface {
|
||||
SvrPlaygroundScreen(
|
||||
state = InternalSvrPlaygroundState(
|
||||
options = persistentListOf(SvrImplementation.SVR1, SvrImplementation.SVR2)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.internal.svr
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class InternalSvrPlaygroundState(
|
||||
val options: ImmutableList<SvrImplementation>,
|
||||
val selected: SvrImplementation = options[0],
|
||||
val loading: Boolean = false,
|
||||
val userPin: String = "",
|
||||
val lastResult: String? = null
|
||||
)
|
||||
|
||||
enum class SvrImplementation(
|
||||
val title: String
|
||||
) {
|
||||
SVR1("KBS"), SVR2("SVR2")
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.svr
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV1
|
||||
|
||||
class InternalSvrPlaygroundViewModel : ViewModel() {
|
||||
|
||||
private val _state: MutableState<InternalSvrPlaygroundState> = mutableStateOf(
|
||||
InternalSvrPlaygroundState(
|
||||
options = persistentListOf(SvrImplementation.SVR1, SvrImplementation.SVR2)
|
||||
)
|
||||
)
|
||||
val state: State<InternalSvrPlaygroundState> = _state
|
||||
|
||||
private val disposables: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
fun onTabSelected(svr: SvrImplementation) {
|
||||
_state.value = _state.value.copy(
|
||||
selected = svr,
|
||||
lastResult = null
|
||||
)
|
||||
}
|
||||
|
||||
fun onPinChanged(pin: String) {
|
||||
_state.value = _state.value.copy(
|
||||
userPin = pin
|
||||
)
|
||||
}
|
||||
|
||||
fun onCreateClicked() {
|
||||
_state.value = _state.value.copy(
|
||||
loading = true
|
||||
)
|
||||
|
||||
disposables += _state.value.selected.toImplementation()
|
||||
.setPin(_state.value.userPin, SignalStore.kbsValues().getOrCreateMasterKey())
|
||||
.execute()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { response ->
|
||||
_state.value = _state.value.copy(
|
||||
loading = false,
|
||||
lastResult = "${response.javaClass.simpleName}\n\n$response"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRestoreClicked() {
|
||||
_state.value = _state.value.copy(
|
||||
loading = true
|
||||
)
|
||||
|
||||
disposables += _state.value.selected.toImplementation()
|
||||
.restoreDataPostRegistration(_state.value.userPin)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { response ->
|
||||
_state.value = _state.value.copy(
|
||||
loading = false,
|
||||
lastResult = "${response.javaClass.simpleName}\n\n$response"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteClicked() {
|
||||
_state.value = _state.value.copy(
|
||||
loading = true
|
||||
)
|
||||
|
||||
disposables += _state.value.selected.toImplementation()
|
||||
.deleteData()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { response ->
|
||||
_state.value = _state.value.copy(
|
||||
loading = false,
|
||||
lastResult = "${response.javaClass.simpleName}\n\n$response"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
private fun SvrImplementation.toImplementation(): SecureValueRecovery {
|
||||
return when (this) {
|
||||
SvrImplementation.SVR1 -> SecureValueRecoveryV1(ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE))
|
||||
SvrImplementation.SVR2 -> ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -584,6 +584,9 @@
|
|||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalSearchFragment"
|
||||
app:destination="@id/internalSearchFragment" />
|
||||
<action
|
||||
android:id="@+id/action_internalSettingsFragment_to_internalSvrPlaygroundFragment"
|
||||
app:destination="@id/internalSvrPlaygroundFragment" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
|
@ -601,6 +604,11 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.search.InternalSearchFragment"
|
||||
android:label="internal_search_fragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/internalSvrPlaygroundFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.internal.svr.InternalSvrPlaygroundFragment"
|
||||
android:label="internal_svr_playground_fragment" />
|
||||
|
||||
<!-- endregion -->
|
||||
|
||||
<!-- Subscriptions -->
|
||||
|
|
|
@ -105,13 +105,15 @@ object Rows {
|
|||
modifier: Modifier = Modifier,
|
||||
iconModifier: Modifier = Modifier,
|
||||
icon: ImageVector? = null,
|
||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface
|
||||
foregroundTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||
onClick: (() -> Unit)? = null
|
||||
) {
|
||||
if (icon != null) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultPadding())
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
|
@ -134,6 +136,7 @@ object Rows {
|
|||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(defaultPadding())
|
||||
.clickable(enabled = onClick != null, onClick = onClick ?: {})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,9 @@ public final class MasterKey {
|
|||
public int hashCode() {
|
||||
return Arrays.hashCode(masterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MasterKey(HashCode: " + hashCode() + ")";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import java.io.IOException
|
||||
|
||||
interface SecureValueRecovery {
|
||||
/**
|
||||
* 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 userPin The user-specified PIN.
|
||||
* @param masterKey The data to set on SVR.
|
||||
*/
|
||||
fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession
|
||||
|
||||
/**
|
||||
* 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: AuthCredentials, userPin: String): Single<RestoreResponse>
|
||||
|
||||
/**
|
||||
* 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(userPin: String): Single<RestoreResponse>
|
||||
|
||||
/**
|
||||
* Deletes the user's SVR data from the service.
|
||||
*/
|
||||
fun deleteData(): Single<DeleteResponse>
|
||||
|
||||
interface PinChangeSession {
|
||||
fun execute(): Single<BackupResponse>
|
||||
}
|
||||
|
||||
/** Response for setting a PIN. */
|
||||
sealed class BackupResponse {
|
||||
/** Operation completed successfully. */
|
||||
data class Success(val masterKey: MasterKey) : 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()
|
||||
|
||||
/** The target enclave could not be found. */
|
||||
object EnclaveNotFound : BackupResponse()
|
||||
|
||||
/** The server rejected the request with a 508. Do not retry. */
|
||||
object ServerRejected : 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()
|
||||
|
||||
/** The target enclave could not be found. */
|
||||
object EnclaveNotFound : DeleteResponse()
|
||||
|
||||
/** The server rejected the request with a 508. Do not retry. */
|
||||
object ServerRejected : 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,108 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.svr
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.libsignal.svr2.PinHash
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.KeyBackupServicePinException
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
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.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
|
||||
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* An implementation of the [SecureValueRecovery] interface backed by the [KeyBackupService].
|
||||
*/
|
||||
class SecureValueRecoveryV1(private val kbs: KeyBackupService) : SecureValueRecovery {
|
||||
|
||||
override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return Svr1PinChangeSession(userPin, masterKey)
|
||||
}
|
||||
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): Single<RestoreResponse> {
|
||||
return restoreData(Single.just(authorization.asBasic()), userPin)
|
||||
}
|
||||
|
||||
override fun restoreDataPostRegistration(userPin: String): Single<RestoreResponse> {
|
||||
return restoreData(Single.fromCallable { kbs.authorization }, userPin)
|
||||
}
|
||||
|
||||
override fun deleteData(): Single<DeleteResponse> {
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
kbs.newPinChangeSession().removePin()
|
||||
DeleteResponse.Success
|
||||
} catch (e: UnauthenticatedResponseException) {
|
||||
DeleteResponse.ApplicationError(e)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
when (e.code) {
|
||||
404 -> DeleteResponse.EnclaveNotFound
|
||||
508 -> DeleteResponse.ServerRejected
|
||||
else -> DeleteResponse.NetworkError(e)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
DeleteResponse.NetworkError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreData(authorization: Single<String>, userPin: String): Single<RestoreResponse> {
|
||||
return authorization
|
||||
.flatMap { auth ->
|
||||
Single.fromCallable {
|
||||
try {
|
||||
val session = kbs.newRegistrationSession(auth, null)
|
||||
val pinHash: PinHash = PinHashUtil.hashPin(userPin, session.hashSalt())
|
||||
|
||||
val data: KbsPinData = session.restorePin(pinHash)
|
||||
RestoreResponse.Success(data.masterKey)
|
||||
} catch (e: KeyBackupSystemNoDataException) {
|
||||
RestoreResponse.Missing
|
||||
} catch (e: KeyBackupServicePinException) {
|
||||
RestoreResponse.PinMismatch(e.triesRemaining)
|
||||
} catch (e: IOException) {
|
||||
RestoreResponse.NetworkError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class Svr1PinChangeSession(
|
||||
private val userPin: String,
|
||||
private val masterKey: MasterKey
|
||||
) : PinChangeSession {
|
||||
override fun execute(): Single<BackupResponse> {
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val session = kbs.newPinChangeSession()
|
||||
val pinHash: PinHash = PinHashUtil.hashPin(userPin, session.hashSalt())
|
||||
|
||||
val data: KbsPinData = session.setPin(pinHash, masterKey)
|
||||
BackupResponse.Success(data.masterKey)
|
||||
} catch (e: UnauthenticatedResponseException) {
|
||||
BackupResponse.ApplicationError(e)
|
||||
} catch (e: NonSuccessfulResponseCodeException) {
|
||||
when (e.code) {
|
||||
404 -> BackupResponse.EnclaveNotFound
|
||||
508 -> BackupResponse.ServerRejected
|
||||
else -> BackupResponse.NetworkError(e)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
BackupResponse.NetworkError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,11 @@ 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.api.svr.SecureValueRecovery.BackupResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteResponse
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.InvalidRequestException
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession
|
||||
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket
|
||||
|
@ -27,7 +32,7 @@ class SecureValueRecoveryV2(
|
|||
private val serviceConfiguration: SignalServiceConfiguration,
|
||||
private val mrEnclave: String,
|
||||
private val pushServiceSocket: PushServiceSocket
|
||||
) {
|
||||
) : SecureValueRecovery {
|
||||
|
||||
/**
|
||||
* Begins a PIN change.
|
||||
|
@ -42,8 +47,8 @@ class SecureValueRecoveryV2(
|
|||
* @param pin The user-specified PIN.
|
||||
* @param masterKey The data to set on SVR.
|
||||
*/
|
||||
fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return PinChangeSession(userPin, masterKey)
|
||||
override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession {
|
||||
return Svr2PinChangeSession(userPin, masterKey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,21 +58,21 @@ class SecureValueRecoveryV2(
|
|||
*
|
||||
* If the user is already registered, use [restoreDataPostRegistration]
|
||||
*/
|
||||
fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): Single<RestoreResponse> {
|
||||
override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): Single<RestoreResponse> {
|
||||
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]
|
||||
*/
|
||||
fun restoreDataPostRegistration(userPin: String): Single<RestoreResponse> {
|
||||
override fun restoreDataPostRegistration(userPin: String): Single<RestoreResponse> {
|
||||
return restoreData(getAuthorization(), userPin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the user's SVR data from the service.
|
||||
*/
|
||||
fun deleteData(): Single<DeleteResponse> {
|
||||
override fun deleteData(): Single<DeleteResponse> {
|
||||
val request: (Svr2PinHasher) -> Request = { Request(delete = DeleteRequest()) }
|
||||
|
||||
return getAuthorization()
|
||||
|
@ -149,16 +154,16 @@ class SecureValueRecoveryV2(
|
|||
* 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(
|
||||
inner class Svr2PinChangeSession(
|
||||
val userPin: String,
|
||||
val masterKey: MasterKey,
|
||||
private var setupComplete: Boolean = false
|
||||
) {
|
||||
) : PinChangeSession {
|
||||
|
||||
/**
|
||||
* Performs the PIN change operation. This is safe to call repeatedly if you get back a retryable error.
|
||||
*/
|
||||
fun execute(): Single<BackupResponse> {
|
||||
override fun execute(): Single<BackupResponse> {
|
||||
val normalizedPin: ByteArray = PinHashUtil.normalize(userPin)
|
||||
|
||||
return getAuthorization()
|
||||
|
@ -233,7 +238,7 @@ class SecureValueRecoveryV2(
|
|||
.map { (response, _) ->
|
||||
when (response.expose?.status) {
|
||||
ProtoExposeResponse.Status.OK -> {
|
||||
BackupResponse.Success
|
||||
BackupResponse.Success(masterKey)
|
||||
}
|
||||
ProtoExposeResponse.Status.ERROR -> {
|
||||
BackupResponse.ExposeFailure
|
||||
|
@ -245,52 +250,4 @@ class SecureValueRecoveryV2(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Response for setting a PIN. */
|
||||
sealed class BackupResponse {
|
||||
/** Operation completed successfully. */
|
||||
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. */
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue