Add mostly-working SVR3 implementation behind flag.

This commit is contained in:
Greyson Parrelli
2024-06-07 15:40:53 -04:00
committed by Alex Hart
parent 143a61e312
commit 664c22d8f1
44 changed files with 1008 additions and 313 deletions

View File

@@ -233,7 +233,7 @@ public final class RegistrationRepository {
.map(BackupAuthCheckProcessor::new)
.doOnSuccess(processor -> {
Log.d(TAG, "Received SVR backup auth credential response.");
if (SignalStore.svr().removeAuthTokens(processor.getInvalid())) {
if (SignalStore.svr().removeSvr2AuthTokens(processor.getInvalid())) {
new BackupManager(context).dataChanged();
}
});

View File

@@ -21,7 +21,7 @@ sealed class VerifyResponseProcessor(response: ServiceResponse<VerifyResponse>)
get() {
return error?.let {
if (it is LockedException) {
SvrAuthCredentialSet(it.svr1Credentials, it.svr2Credentials)
SvrAuthCredentialSet(svr2Credentials = it.svr2Credentials, svr3Credentials = it.svr3Credentials)
} else {
null
}
@@ -65,7 +65,7 @@ sealed class VerifyResponseProcessor(response: ServiceResponse<VerifyResponse>)
*/
class VerifyResponseWithoutKbs(response: ServiceResponse<VerifyResponse>) : VerifyResponseProcessor(response) {
override fun isRegistrationLockPresentAndSvrExhausted(): Boolean {
return registrationLock() && getLockedException().svr1Credentials == null && getLockedException().svr2Credentials == null
return registrationLock() && getLockedException().svr2Credentials == null
}
}

View File

@@ -10,7 +10,6 @@ import android.content.Context
import androidx.core.app.NotificationManagerCompat
import com.google.android.gms.auth.api.phone.SmsRetriever
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
@@ -37,6 +36,7 @@ import org.thoughtcrime.securesms.jobs.RotateCertificateJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.pin.Svr3Migration
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.pin.SvrWrongPinException
import org.thoughtcrime.securesms.push.AccountManagerFactory
@@ -67,6 +67,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.registration.RegistrationApi
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataHeaders
@@ -259,9 +260,10 @@ object RegistrationRepository {
return PinHashUtil.verifyLocalPinHash(pinHash, pin)
}
suspend fun fetchMasterKeyFromSvrRemote(pin: String, authCredentials: AuthCredentials): MasterKey =
suspend fun fetchMasterKeyFromSvrRemote(pin: String, svr2Credentials: AuthCredentials?, svr3Credentials: Svr3Credentials?): MasterKey =
withContext(Dispatchers.IO) {
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, authCredentials), pin)
val credentialSet = SvrAuthCredentialSet(svr2Credentials = svr2Credentials, svr3Credentials = svr3Credentials)
val masterKey = SvrRepository.restoreMasterKeyPreRegistration(credentialSet, pin)
SignalStore.svr().setMasterKey(masterKey, pin)
return@withContext masterKey
}
@@ -488,36 +490,55 @@ object RegistrationRepository {
suspend fun hasValidSvrAuthCredentials(context: Context, e164: String, password: String): BackupAuthCheckResult =
withContext(Dispatchers.IO) {
val usernamePasswords = async { retrieveLocalSvrCredentials() }
val api: RegistrationApi = AccountManagerFactory.getInstance().createUnauthenticated(context, e164, SignalServiceAddress.DEFAULT_DEVICE_ID, password).registrationApi
val authTokens = usernamePasswords.await()
if (authTokens.isEmpty()) {
return@withContext BackupAuthCheckResult.SuccessWithoutCredentials()
}
val result = api.getSvrAuthCredential(e164, authTokens)
.runIfSuccessful {
val removedInvalidTokens = SignalStore.svr().removeAuthTokens(it.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
val svr3Result = SignalStore.svr().svr3AuthTokens
?.takeIf { Svr3Migration.shouldReadFromSvr3 }
?.takeIf { it.isNotEmpty() }
?.toSvrCredentials()
?.let { authTokens ->
api
.validateSvr3AuthCredential(e164, authTokens)
.runIfSuccessful {
val removedInvalidTokens = SignalStore.svr().removeSvr3AuthTokens(it.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
}
.let { BackupAuthCheckResult.fromV3(it) }
}
return@withContext BackupAuthCheckResult.from(result)
if (svr3Result is BackupAuthCheckResult.SuccessWithCredentials) {
Log.d(TAG, "Found valid SVR3 credentials.")
return@withContext svr3Result
}
Log.d(TAG, "No valid SVR3 credentials, looking for SVR2.")
return@withContext SignalStore.svr().svr2AuthTokens
?.takeIf { it.isNotEmpty() }
?.toSvrCredentials()
?.let { authTokens ->
api
.validateSvr2AuthCredential(e164, authTokens)
.runIfSuccessful {
val removedInvalidTokens = SignalStore.svr().removeSvr2AuthTokens(it.invalid)
if (removedInvalidTokens) {
BackupManager(context).dataChanged()
}
}
.let { BackupAuthCheckResult.fromV2(it) }
} ?: BackupAuthCheckResult.SuccessWithoutCredentials()
}
private suspend fun retrieveLocalSvrCredentials(): List<String> = withContext(Dispatchers.IO) {
return@withContext SignalStore.svr()
.authTokenList
/** Converts the basic-auth creds we have locally into username:password pairs that are suitable for handing off to the service. */
private fun List<String?>.toSvrCredentials(): List<String> {
return this
.asSequence()
.filterNotNull()
.take<String>(10)
.map<String, String> {
it.replace("Basic ", "").trim()
}
.mapNotNull<String, ByteArray> {
.take(10)
.map { it.replace("Basic ", "").trim() }
.mapNotNull {
try {
Base64.decode(it)
} catch (e: IOException) {
@@ -525,9 +546,7 @@ object RegistrationRepository {
null
}
}
.map<ByteArray, String> {
String(it, StandardCharsets.ISO_8859_1)
}
.map { String(it, StandardCharsets.ISO_8859_1) }
.toList()
}

View File

@@ -6,21 +6,41 @@
package org.thoughtcrime.securesms.registration.v2.data.network
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse
import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse
import org.whispersystems.signalservice.internal.push.BackupV3AuthCheckResponse
/**
* This is a processor to map a [BackupAuthCheckResponse] to all the known outcomes.
* This is a processor to map a [BackupV2AuthCheckResponse] to all the known outcomes.
*/
sealed class BackupAuthCheckResult(cause: Throwable?) : RegistrationResult(cause) {
companion object {
@JvmStatic
fun from(networkResult: NetworkResult<BackupAuthCheckResponse>): BackupAuthCheckResult {
fun fromV2(networkResult: NetworkResult<BackupV2AuthCheckResponse>): BackupAuthCheckResult {
return when (networkResult) {
is NetworkResult.Success -> {
val match = networkResult.result.match
if (match != null) {
SuccessWithCredentials(match)
SuccessWithCredentials(svr2Credentials = match, svr3Credentials = null)
} else {
SuccessWithoutCredentials()
}
}
is NetworkResult.ApplicationError -> UnknownError(networkResult.throwable)
is NetworkResult.NetworkError -> UnknownError(networkResult.exception)
is NetworkResult.StatusCodeError -> UnknownError(networkResult.exception)
}
}
@JvmStatic
fun fromV3(networkResult: NetworkResult<BackupV3AuthCheckResponse>): BackupAuthCheckResult {
return when (networkResult) {
is NetworkResult.Success -> {
val match = networkResult.result.match
if (match != null) {
SuccessWithCredentials(svr2Credentials = null, svr3Credentials = match)
} else {
SuccessWithoutCredentials()
}
@@ -33,7 +53,7 @@ sealed class BackupAuthCheckResult(cause: Throwable?) : RegistrationResult(cause
}
}
class SuccessWithCredentials(val authCredentials: AuthCredentials) : BackupAuthCheckResult(null)
class SuccessWithCredentials(val svr2Credentials: AuthCredentials?, val svr3Credentials: Svr3Credentials?) : BackupAuthCheckResult(null)
class SuccessWithoutCredentials : BackupAuthCheckResult(null)

View File

@@ -13,6 +13,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
@@ -33,7 +34,7 @@ sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause
is AuthorizationFailedException -> AuthorizationFailed(cause)
is MalformedRequestException -> MalformedRequest(cause)
is RateLimitException -> createRateLimitProcessor(cause)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials)
else -> {
if (networkResult.code == 422) {
ValidationError(cause)
@@ -61,7 +62,7 @@ sealed class RegisterAccountResult(cause: Throwable?) : RegistrationResult(cause
class ValidationError(cause: Throwable) : RegisterAccountResult(cause)
class RateLimited(cause: Throwable, val timeRemaining: Long) : RegisterAccountResult(cause)
class AttemptsExhausted(cause: Throwable) : RegisterAccountResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials?) : RegisterAccountResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials?, val svr3Credentials: Svr3Credentials?) : RegisterAccountResult(cause)
class UnknownError(cause: Throwable) : RegisterAccountResult(cause)
class SvrNoData(cause: SvrNoDataException) : RegisterAccountResult(cause)

View File

@@ -21,6 +21,7 @@ import org.whispersystems.signalservice.api.push.exceptions.PushChallengeRequire
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.api.push.exceptions.RegistrationRetryException
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataJson
@@ -70,7 +71,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
is InvalidTransportModeException -> InvalidTransportModeFailure(cause)
is MalformedRequestException -> MalformedRequest(cause)
is RegistrationRetryException -> MustRetry(cause)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials)
is LockedException -> RegistrationLocked(cause = cause, timeRemaining = cause.timeRemaining, svr2Credentials = cause.svr2Credentials, svr3Credentials = cause.svr3Credentials)
is NoSuchSessionException -> NoSuchSession(cause)
is AlreadyVerifiedException -> AlreadyVerified(cause)
else -> UnknownError(cause)
@@ -125,7 +126,7 @@ sealed class VerificationCodeRequestResult(cause: Throwable?) : RegistrationResu
class MustRetry(cause: Throwable) : VerificationCodeRequestResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials) : VerificationCodeRequestResult(cause)
class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials, val svr3Credentials: Svr3Credentials) : VerificationCodeRequestResult(cause)
class NoSuchSession(cause: Throwable) : VerificationCodeRequestResult(cause)

View File

@@ -11,6 +11,7 @@ import com.google.i18n.phonenumbers.Phonenumber
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.v2.data.network.Challenge
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
/**
@@ -24,7 +25,8 @@ data class RegistrationV2State(
val isReRegister: Boolean = false,
val recoveryPassword: String? = SignalStore.svr().getRecoveryPassword(),
val canSkipSms: Boolean = false,
val svrAuthCredentials: AuthCredentials? = null,
val svr2AuthCredentials: AuthCredentials? = null,
val svr3AuthCredentials: Svr3Credentials? = null,
val svrTriesRemaining: Int = 10,
val incorrectCodeAttempts: Int = 0,
val isRegistrationLockEnabled: Boolean = false,

View File

@@ -229,7 +229,13 @@ class RegistrationV2ViewModel : ViewModel() {
is BackupAuthCheckResult.SuccessWithCredentials -> {
Log.d(TAG, "Found local valid SVR auth credentials.")
store.update {
it.copy(isReRegister = true, canSkipSms = true, svrAuthCredentials = svrCredentialsResult.authCredentials, inProgress = false)
it.copy(
isReRegister = true,
canSkipSms = true,
svr2AuthCredentials = svrCredentialsResult.svr2Credentials,
svr3AuthCredentials = svrCredentialsResult.svr3Credentials,
inProgress = false
)
}
return@launch
}
@@ -571,12 +577,14 @@ class RegistrationV2ViewModel : ViewModel() {
}
// remote recovery password
val authCredentials = store.value.svrAuthCredentials
if (authCredentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR.")
val svr2Credentials = store.value.svr2AuthCredentials
val svr3Credentials = store.value.svr3AuthCredentials
if (svr2Credentials != null || svr3Credentials != null) {
Log.d(TAG, "Found SVR auth credentials, fetching recovery password from SVR (svr2: ${svr2Credentials != null}, svr3: ${svr3Credentials != null}).")
viewModelScope.launch(context = coroutineExceptionHandler) {
try {
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, authCredentials)
val masterKey = RegistrationRepository.fetchMasterKeyFromSvrRemote(pin, svr2Credentials, svr3Credentials)
setRecoveryPassword(masterKey.deriveRegistrationRecoveryPassword())
updateSvrTriesRemaining(10)
verifyReRegisterInternal(context, pin, masterKey, registrationErrorHandler)
@@ -628,7 +636,10 @@ class RegistrationV2ViewModel : ViewModel() {
Log.i(TAG, "Received a registration lock response when trying to register an account. Retrying with master key.")
store.update {
it.copy(svrAuthCredentials = registrationResult.svr2Credentials)
it.copy(
svr2AuthCredentials = registrationResult.svr2Credentials,
svr3AuthCredentials = registrationResult.svr3Credentials
)
}
return Pair(RegistrationRepository.registerAccount(context = context, sessionId = sessionId, registrationData = registrationData, pin = pin) { masterKey }, true)
@@ -716,19 +727,26 @@ class RegistrationV2ViewModel : ViewModel() {
if (pin == null && SignalStore.svr().registrationLockToken != null) {
Log.d(TAG, "Retrying registration with stored credentials.")
result = RegistrationRepository.registerAccount(context, sessionId, registrationData, SignalStore.svr().pin) { SignalStore.svr().getOrCreateMasterKey() }
} else if (result.svr2Credentials != null) {
Log.d(TAG, "Retrying registration with received credentials.")
val credentials = result.svr2Credentials
} else if (result.svr2Credentials != null || result.svr3Credentials != null) {
Log.d(TAG, "Retrying registration with received credentials (svr2: ${result.svr2Credentials != null}, svr3: ${result.svr3Credentials != null}).")
val svr2Credentials = result.svr2Credentials
val svr3Credentials = result.svr3Credentials
state = store.updateAndGet {
it.copy(svrAuthCredentials = credentials)
it.copy(svr2AuthCredentials = svr2Credentials, svr3AuthCredentials = svr3Credentials)
}
}
}
if (reglock && pin.isNotNullOrBlank()) {
Log.d(TAG, "Registration lock enabled, attempting to register account restore master key from SVR.")
Log.d(TAG, "Registration lock enabled, attempting to register account restore master key from SVR (svr2: ${state.svr2AuthCredentials != null}, svr3: ${state.svr3AuthCredentials != null})")
result = RegistrationRepository.registerAccount(context, sessionId, registrationData, pin) {
SvrRepository.restoreMasterKeyPreRegistration(SvrAuthCredentialSet(null, state.svrAuthCredentials), pin)
SvrRepository.restoreMasterKeyPreRegistration(
credentials = SvrAuthCredentialSet(
svr2Credentials = state.svr2AuthCredentials,
svr3Credentials = state.svr3AuthCredentials
),
userPin = pin
)
}
}

View File

@@ -353,7 +353,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
}
private Single<Boolean> checkForValidSvrAuthCredentials() {
final List<String> svrAuthTokenList = SignalStore.svr().getAuthTokenList();
final List<String> svrAuthTokenList = SignalStore.svr().getSvr2AuthTokens();
List<String> usernamePasswords = svrAuthTokenList
.stream()
.limit(10)
@@ -376,7 +376,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
.flatMap(p -> {
if (p.hasValidSvr2AuthCredential()) {
Log.d(TAG, "Saving valid SVR2 auth credential.");
setSvrAuthCredentials(new SvrAuthCredentialSet(null, p.requireSvr2AuthCredential()));
setSvrAuthCredentials(new SvrAuthCredentialSet(p.requireSvr2AuthCredential(), null));
return Single.just(true);
} else {
Log.d(TAG, "SVR2 response contained no valid SVR2 auth credentials.");

View File

@@ -7,20 +7,24 @@ package org.thoughtcrime.securesms.registration.viewmodel
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
@Parcelize
data class SvrAuthCredentialSet(
private val svr1Credentials: ParcelableAuthCredentials?,
private val svr2Credentials: ParcelableAuthCredentials?
private val svr2Credentials: ParcelableAuthCredentials?,
private val svr3Credentials: ParcelableSvr3AuthCredentials?
) : Parcelable {
constructor(
svr1Credentials: AuthCredentials?,
svr2Credentials: AuthCredentials?
) : this(ParcelableAuthCredentials.createOrNull(svr1Credentials), ParcelableAuthCredentials.createOrNull(svr2Credentials))
svr2Credentials: AuthCredentials?,
svr3Credentials: Svr3Credentials?
) : this(
ParcelableAuthCredentials.createOrNull(svr2Credentials),
ParcelableSvr3AuthCredentials.createOrNull(svr3Credentials)
)
val svr1: AuthCredentials? = svr1Credentials?.credentials()
val svr2: AuthCredentials? = svr2Credentials?.credentials()
val svr3: Svr3Credentials? = svr3Credentials?.credentials()
@Parcelize
data class ParcelableAuthCredentials(private val username: String, private val password: String) : Parcelable {
@@ -39,4 +43,22 @@ data class SvrAuthCredentialSet(
return AuthCredentials.create(username, password)
}
}
@Parcelize
data class ParcelableSvr3AuthCredentials(private val username: String, private val password: String, private val shareSet: ByteArray?) : Parcelable {
companion object {
fun createOrNull(creds: Svr3Credentials?): ParcelableSvr3AuthCredentials? {
return if (creds != null) {
ParcelableSvr3AuthCredentials(creds.username, creds.password, creds.shareSet)
} else {
null
}
}
}
fun credentials(): Svr3Credentials {
return Svr3Credentials(username, password, shareSet)
}
}
}