diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt index ab63dbee93..7ddadc1e4f 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt @@ -260,7 +260,8 @@ class ChangeNumberViewModelTest { Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } ) - every { SvrRepository.restoreMasterKeyPreRegistration(any(), any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) + every { SvrRepository.restoreMasterKeyPreRegistrationFromV2(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) + every { SvrRepository.restoreMasterKeyPreRegistrationFromV3(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) // WHEN viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow @@ -369,7 +370,8 @@ class ChangeNumberViewModelTest { Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } ) - every { SvrRepository.restoreMasterKeyPreRegistration(any(), any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) + every { SvrRepository.restoreMasterKeyPreRegistrationFromV2(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) + every { SvrRepository.restoreMasterKeyPreRegistrationFromV3(any(), any()) } returns SecureValueRecovery.RestoreResponse.Success(MasterKey.createNew(SecureRandom()), AuthCredentials.create("username", "password")) // WHEN viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow diff --git a/app/src/main/java/org/thoughtcrime/securesms/absbackup/backupables/SvrAuthTokens.kt b/app/src/main/java/org/thoughtcrime/securesms/absbackup/backupables/SvrAuthTokens.kt index 94525ea16d..6d79064e02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/absbackup/backupables/SvrAuthTokens.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/absbackup/backupables/SvrAuthTokens.kt @@ -17,19 +17,19 @@ object SvrAuthTokens : AndroidBackupItem { } override fun getDataForBackup(): ByteArray { - val proto = SvrAuthToken(tokens = SignalStore.svr().authTokenList) + val proto = SvrAuthToken(svr2Tokens = SignalStore.svr().svr2AuthTokens) return proto.encode() } override fun restoreData(data: ByteArray) { - if (SignalStore.svr().authTokenList.isNotEmpty()) { + if (SignalStore.svr().svr2AuthTokens.isNotEmpty()) { return } try { val proto = SvrAuthToken.ADAPTER.decode(data) - SignalStore.svr().putAuthTokenList(proto.tokens) + SignalStore.svr().putSvr2AuthTokens(proto.svr2Tokens) } catch (e: IOException) { Log.w(TAG, "Cannot restore KbsAuthToken from backup service.") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt index 8edb51a798..56c6923eca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberResult.kt @@ -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 ChangeNumberResult(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) @@ -62,7 +63,7 @@ sealed class ChangeNumberResult(cause: Throwable?) : RegistrationResult(cause) { class ValidationError(cause: Throwable) : ChangeNumberResult(cause) class RateLimited(cause: Throwable, val timeRemaining: Long) : ChangeNumberResult(cause) class AttemptsExhausted(cause: Throwable) : ChangeNumberResult(cause) - class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials?) : ChangeNumberResult(cause) + class RegistrationLocked(cause: Throwable, val timeRemaining: Long, val svr2Credentials: AuthCredentials?, val svr3Credentials: Svr3Credentials?) : ChangeNumberResult(cause) class UnknownError(cause: Throwable) : ChangeNumberResult(cause) class SvrNoData(cause: SvrNoDataException) : ChangeNumberResult(cause) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt index 459f773b5a..3f417bd4f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberState.kt @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber.v2 import org.thoughtcrime.securesms.registration.v2.data.network.Challenge import org.thoughtcrime.securesms.registration.v2.data.network.VerificationCodeRequestResult import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState +import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.internal.push.AuthCredentials /** @@ -21,7 +22,8 @@ data class ChangeNumberState( val sessionId: String? = null, val changeNumberOutcome: ChangeNumberOutcome? = null, val lockedTimeRemaining: Long = 0L, - val svrCredentials: AuthCredentials? = null, + val svr2Credentials: AuthCredentials? = null, + val svr3Credentials: Svr3Credentials? = null, val svrTriesRemaining: Int = 10, val incorrectCodeAttempts: Int = 0, val nextSmsTimestamp: Long = 0L, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt index 459af31ff4..f05c379e72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/v2/ChangeNumberV2ViewModel.kt @@ -253,7 +253,15 @@ class ChangeNumberV2ViewModel : ViewModel() { val result: ChangeNumberResult = if (pin == null) { repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number) } else { - repository.changeNumberWithRegistrationLock(sessionId = sessionId, newE164 = number.e164Number, pin, SvrAuthCredentialSet(null, store.value.svrCredentials)) + repository.changeNumberWithRegistrationLock( + sessionId = sessionId, + newE164 = number.e164Number, + pin = pin, + svrAuthCredentials = SvrAuthCredentialSet( + svr2Credentials = store.value.svr2Credentials, + svr3Credentials = store.value.svr3Credentials + ) + ) } if (result is ChangeNumberResult.Success) { @@ -422,7 +430,8 @@ class ChangeNumberV2ViewModel : ViewModel() { is VerificationCodeRequestResult.RegistrationLocked -> store.update { it.copy( - svrCredentials = result.svr2Credentials + svr2Credentials = result.svr2Credentials, + svr3Credentials = result.svr3Credentials ) } else -> Log.i(TAG, "Received exception during verification.", result.getCause()) @@ -438,7 +447,8 @@ class ChangeNumberV2ViewModel : ViewModel() { is ChangeNumberResult.RegistrationLocked -> store.update { it.copy( - svrCredentials = result.svr2Credentials + svr2Credentials = result.svr2Credentials, + svr3Credentials = result.svr3Credentials ) } is ChangeNumberResult.SvrWrongPin -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt index 569beda01b..6c60d1fc1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundFragment.kt @@ -21,7 +21,6 @@ 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt index 5ecdfe1804..97ef24aba9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/svr/InternalSvrPlaygroundViewModel.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore import org.whispersystems.signalservice.api.svr.SecureValueRecovery -import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3 class InternalSvrPlaygroundViewModel : ViewModel() { @@ -105,22 +104,7 @@ class InternalSvrPlaygroundViewModel : ViewModel() { private fun SvrImplementation.toImplementation(): SecureValueRecovery { return when (this) { SvrImplementation.SVR2 -> AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE) - SvrImplementation.SVR3 -> AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV3(AppDependencies.libsignalNetwork, TestShareSetStorage()) - } - } - - /** - * Temporary implementation of share set storage. Only useful for testing. - */ - private class TestShareSetStorage : SecureValueRecoveryV3.ShareSetStorage { - private var shareSet: ByteArray? = null - - override fun write(data: ByteArray) { - shareSet = data - } - - override fun read(): ByteArray? { - return shareSet + SvrImplementation.SVR3 -> AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV3(AppDependencies.libsignalNetwork) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java index f1da3970e2..bfe19b034d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Job.java @@ -218,12 +218,10 @@ public abstract class Job { return resultType == ResultType.SUCCESS; } - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) public boolean isRetry() { return resultType == ResultType.RETRY; } - @VisibleForTesting(otherwise = PACKAGE_PRIVATE) public boolean isFailure() { return resultType == ResultType.FAILURE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 7813b7d707..92a9d47205 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -239,6 +239,7 @@ public final class JobManagerFactories { put(StoryOnboardingDownloadJob.KEY, new StoryOnboardingDownloadJob.Factory()); put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory()); put(Svr2MirrorJob.KEY, new Svr2MirrorJob.Factory()); + put(Svr3MirrorJob.KEY, new Svr3MirrorJob.Factory()); put(SyncArchivedMediaJob.KEY, new SyncArchivedMediaJob.Factory()); put(ThreadUpdateJob.KEY, new ThreadUpdateJob.Factory()); put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResetSvrGuessCountJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResetSvrGuessCountJob.kt index 93023a1428..fc086e5d1e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ResetSvrGuessCountJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ResetSvrGuessCountJob.kt @@ -11,11 +11,13 @@ import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JsonJobData import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.Svr3Migration import org.thoughtcrime.securesms.pin.SvrRepository import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.svr.SecureValueRecovery import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession +import org.whispersystems.signalservice.internal.push.AuthCredentials import kotlin.concurrent.withLock import kotlin.time.Duration.Companion.days @@ -24,8 +26,10 @@ import kotlin.time.Duration.Companion.days */ class ResetSvrGuessCountJob private constructor( parameters: Parameters, - private val serializedChangeSession: String?, - private var svr2Complete: Boolean + private var serializedChangeSessionV2: String?, + private var serializedChangeSessionV3: String?, + private var svr2Complete: Boolean, + private var svr3Complete: Boolean ) : Job(parameters) { companion object { @@ -33,8 +37,10 @@ class ResetSvrGuessCountJob private constructor( private val TAG = Log.tag(ResetSvrGuessCountJob::class.java) - private const val KEY_CHANGE_SESSION = "change_session" + private const val KEY_CHANGE_SESSION_V2 = "change_session" + private const val KEY_CHANGE_SESSION_V3 = "change_session_v3" private const val KEY_SVR2_COMPLETE = "svr2_complete" + private const val KEY_SVR3_COMPLETE = "svr3_complete" } constructor() : this( @@ -46,13 +52,17 @@ class ResetSvrGuessCountJob private constructor( .setMaxInstancesForFactory(1) .build(), null, + null, + false, false ) override fun serialize(): ByteArray? { return JsonJobData.Builder() - .putString(KEY_CHANGE_SESSION, serializedChangeSession) + .putString(KEY_CHANGE_SESSION_V2, serializedChangeSessionV2) + .putString(KEY_CHANGE_SESSION_V3, serializedChangeSessionV3) .putBoolean(KEY_SVR2_COMPLETE, svr2Complete) + .putBoolean(KEY_SVR3_COMPLETE, svr3Complete) .build() .serialize() } @@ -75,30 +85,70 @@ class ResetSvrGuessCountJob private constructor( val masterKey: MasterKey = SignalStore.svr().getOrCreateMasterKey() - val svr2Result = if (!svr2Complete) { - resetGuessCount(AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE), pin, masterKey) - } else { - Log.d(TAG, "Already reset guess count on SVR2. Skipping.") + val svr3Result = if (svr3Complete) { + Log.d(TAG, "Already reset guess count on SVR3. Skipping.") Result.success() + } else if (!Svr3Migration.shouldWriteToSvr3) { + Log.d(TAG, "SVR3 writes disabled. Skipping.") + Result.success() + } else { + Log.d(TAG, "Resetting count on SVR3...") + resetGuessCount( + svr = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV3(AppDependencies.libsignalNetwork), + serializedChangeSession = serializedChangeSessionV3, + pin = pin, + masterKey = masterKey, + changeSessionSaver = { serializedChangeSessionV3 = it.serialize() }, + authTokenSaver = { SignalStore.svr().appendSvr3AuthTokenToList(it.asBasic()) } + ) } - return svr2Result + if (svr3Result.isRetry) { + return svr3Result + } + + return if (svr2Complete) { + Log.d(TAG, "Already reset guess count on SVR2. Skipping.") + Result.success() + } else if (!Svr3Migration.shouldWriteToSvr2) { + Log.d(TAG, "SVR2 writes disabled. Skipping.") + Result.success() + } else { + Log.d(TAG, "Resetting count on SVR2...") + resetGuessCount( + svr = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE), + serializedChangeSession = serializedChangeSessionV2, + pin = pin, + masterKey = masterKey, + changeSessionSaver = { serializedChangeSessionV2 = it.serialize() }, + authTokenSaver = { SignalStore.svr().appendSvr2AuthTokenToList(it.asBasic()) } + ) + } } } override fun onFailure() = Unit - private fun resetGuessCount(svr: SecureValueRecovery, pin: String, masterKey: MasterKey): Result { + private fun resetGuessCount( + svr: SecureValueRecovery, + serializedChangeSession: String?, + pin: String, + masterKey: MasterKey, + changeSessionSaver: (PinChangeSession) -> Unit, + authTokenSaver: (AuthCredentials) -> Unit + ): Result { val session: PinChangeSession = if (serializedChangeSession != null) { svr.resumePinChangeSession(pin, SignalStore.svr().getOrCreateMasterKey(), serializedChangeSession) } else { svr.setPin(pin, masterKey) } + changeSessionSaver(session) + return when (val response: BackupResponse = session.execute()) { is BackupResponse.Success -> { Log.i(TAG, "Successfully reset guess count. $svr") - SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic()) + authTokenSaver(response.authorization) Result.success() } is BackupResponse.ApplicationError -> { @@ -130,8 +180,10 @@ class ResetSvrGuessCountJob private constructor( return ResetSvrGuessCountJob( parameters, - data.getString(KEY_CHANGE_SESSION), - data.getBoolean(KEY_SVR2_COMPLETE) + data.getString(KEY_CHANGE_SESSION_V2), + data.getStringOrDefault(KEY_CHANGE_SESSION_V3, null), + data.getBoolean(KEY_SVR2_COMPLETE), + data.getBooleanOrDefault(KEY_SVR3_COMPLETE, false) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr2MirrorJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr2MirrorJob.kt index f531c72f5e..47a119b106 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr2MirrorJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr2MirrorJob.kt @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.JsonJobData import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.Svr3Migration import org.thoughtcrime.securesms.pin.SvrRepository import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse @@ -53,6 +54,11 @@ class Svr2MirrorJob private constructor(parameters: Parameters, private var seri override fun getFactoryKey(): String = KEY override fun run(): Result { + if (!Svr3Migration.shouldWriteToSvr2) { + Log.w(TAG, "Writes to SVR2 are disabled. Skipping.") + return Result.success() + } + SvrRepository.operationLock.withLock { val pin = SignalStore.svr().pin @@ -77,7 +83,7 @@ class Svr2MirrorJob private constructor(parameters: Parameters, private var seri return when (val response: BackupResponse = session.execute()) { is BackupResponse.Success -> { Log.i(TAG, "Successfully migrated to SVR2! $svr2") - SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic()) + SignalStore.svr().appendSvr2AuthTokenToList(response.authorization.asBasic()) AppDependencies.jobManager.add(RefreshAttributesJob()) Result.success() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr3MirrorJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr3MirrorJob.kt new file mode 100644 index 0000000000..68772fab35 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/Svr3MirrorJob.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.protos.Svr3MirrorJobData +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.Svr3Migration +import org.thoughtcrime.securesms.pin.SvrRepository +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.PinChangeSession +import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3 +import kotlin.concurrent.withLock +import kotlin.time.Duration.Companion.days + +/** + * Ensures a user's SVR data is written to SVR3. + */ +class Svr3MirrorJob private constructor(parameters: Parameters, private var serializedChangeSession: String?) : Job(parameters) { + + companion object { + const val KEY = "Svr3MirrorJob" + + private val TAG = Log.tag(Svr3MirrorJob::class.java) + private const val KEY_CHANGE_SESSION = "change_session" + } + + constructor() : this( + Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(30.days.inWholeMilliseconds) + .setMaxAttempts(Parameters.UNLIMITED) + .setQueue("Svr3MirrorJob") + .setMaxInstancesForFactory(1) + .build(), + null + ) + + override fun serialize(): ByteArray? { + return Svr3MirrorJobData( + serializedChangeSession = serializedChangeSession + ).encode() + } + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + if (!Svr3Migration.shouldWriteToSvr3) { + Log.w(TAG, "Writes to SVR3 are disabled. Skipping.") + return Result.success() + } + + SvrRepository.operationLock.withLock { + val pin = SignalStore.svr().pin + + if (SignalStore.svr().hasOptedOut()) { + Log.w(TAG, "Opted out of SVR! Nothing to migrate.") + return Result.success() + } + + if (pin == null) { + Log.w(TAG, "No PIN available! Can't migrate!") + return Result.success() + } + + val svr3: SecureValueRecoveryV3 = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV3(AppDependencies.libsignalNetwork) + + val session: PinChangeSession = serializedChangeSession?.let { session -> + svr3.resumePinChangeSession(pin, SignalStore.svr().getOrCreateMasterKey(), session) + } ?: svr3.setPin(pin, SignalStore.svr().getOrCreateMasterKey()) + + serializedChangeSession = session.serialize() + + return when (val response: BackupResponse = session.execute()) { + is BackupResponse.Success -> { + Log.i(TAG, "Successfully migrated to SVR3! $svr3") + SignalStore.svr().appendSvr3AuthTokenToList(response.authorization.asBasic()) + AppDependencies.jobManager.add(RefreshAttributesJob()) + Result.success() + } + is BackupResponse.ApplicationError -> { + if (response.exception.isUnauthorized()) { + Log.w(TAG, "Unauthorized! Giving up.", response.exception) + Result.success() + } else { + Log.w(TAG, "Hit an application error. Retrying.", response.exception) + Result.retry(defaultBackoff()) + } + } + BackupResponse.EnclaveNotFound -> { + Log.w(TAG, "Could not find the enclave. Giving up.") + Result.success() + } + BackupResponse.ExposeFailure -> { + Log.w(TAG, "Failed to expose the backup. Giving up.") + Result.success() + } + is BackupResponse.NetworkError -> { + Log.w(TAG, "Hit a network error. Retrying.", response.exception) + Result.retry(defaultBackoff()) + } + BackupResponse.ServerRejected -> { + Log.w(TAG, "Server told us to stop trying. Giving up.") + Result.success() + } + } + } + } + + private fun Throwable.isUnauthorized(): Boolean { + return this is NonSuccessfulResponseCodeException && this.code == 401 + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): Svr3MirrorJob { + val jobData: Svr3MirrorJobData? = serializedData?.let { Svr3MirrorJobData.ADAPTER.decode(serializedData) } + return Svr3MirrorJob(parameters, jobData?.serializedChangeSession) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.java index 16ec15fc68..8f40ede5b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SvrValues.java @@ -26,8 +26,9 @@ public final class SvrValues extends SignalStoreValues { private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp"; public static final String OPTED_OUT = "kbs.opted_out"; private static final String PIN_FORGOTTEN_OR_SKIPPED = "kbs.pin.forgotten.or.skipped"; - private static final String SVR_AUTH_TOKENS = "kbs.kbs_auth_tokens"; + private static final String SVR2_AUTH_TOKENS = "kbs.kbs_auth_tokens"; private static final String SVR_LAST_AUTH_REFRESH_TIMESTAMP = "kbs.kbs_auth_tokens.last_refresh_timestamp"; + private static final String SVR3_AUTH_TOKENS = "kbs.svr3_auth_tokens"; SvrValues(KeyValueStore store) { super(store); @@ -40,7 +41,10 @@ public final class SvrValues extends SignalStoreValues { @Override @NonNull List getKeysToIncludeInBackup() { - return List.of(SVR_AUTH_TOKENS); + return List.of( + SVR2_AUTH_TOKENS, + SVR3_AUTH_TOKENS + ); } /** @@ -54,7 +58,7 @@ public final class SvrValues extends SignalStoreValues { .remove(PIN) .remove(LAST_CREATE_FAILED_TIMESTAMP) .remove(OPTED_OUT) - .remove(SVR_AUTH_TOKENS) + .remove(SVR2_AUTH_TOKENS) .remove(SVR_LAST_AUTH_REFRESH_TIMESTAMP) .commit(); } @@ -167,13 +171,22 @@ public final class SvrValues extends SignalStoreValues { putBoolean(PIN_FORGOTTEN_OR_SKIPPED, value); } - public synchronized void putAuthTokenList(List tokens) { - putList(SVR_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE); + public synchronized void putSvr2AuthTokens(List tokens) { + putList(SVR2_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE); setLastRefreshAuthTimestamp(System.currentTimeMillis()); } - public synchronized List getAuthTokenList() { - return getList(SVR_AUTH_TOKENS, StringStringSerializer.INSTANCE); + public synchronized void putSvr3AuthTokens(List tokens) { + putList(SVR3_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE); + setLastRefreshAuthTimestamp(System.currentTimeMillis()); + } + + public synchronized List getSvr2AuthTokens() { + return getList(SVR2_AUTH_TOKENS, StringStringSerializer.INSTANCE); + } + + public synchronized List getSvr3AuthTokens() { + return getList(SVR3_AUTH_TOKENS, StringStringSerializer.INSTANCE); } /** @@ -181,21 +194,47 @@ public final class SvrValues extends SignalStoreValues { * @param token * @return whether the token was added (new) or ignored (already existed) */ - public synchronized boolean appendAuthTokenToList(String token) { - List tokens = getAuthTokenList(); + public synchronized boolean appendSvr2AuthTokenToList(String token) { + List tokens = getSvr2AuthTokens(); if (tokens.contains(token)) { return false; } else { final List result = Stream.concat(Stream.of(token), tokens.stream()).limit(10).collect(Collectors.toList()); - putAuthTokenList(result); + putSvr2AuthTokens(result); return true; } } - public boolean removeAuthTokens(@NonNull List invalid) { - List tokens = new ArrayList<>(getAuthTokenList()); + /** + * Keeps the 10 most recent SVR3 auth tokens. + * @param token + * @return whether the token was added (new) or ignored (already existed) + */ + public synchronized boolean appendSvr3AuthTokenToList(String token) { + List tokens = getSvr3AuthTokens(); + if (tokens.contains(token)) { + return false; + } else { + final List result = Stream.concat(Stream.of(token), tokens.stream()).limit(10).collect(Collectors.toList()); + putSvr3AuthTokens(result); + return true; + } + } + + public boolean removeSvr2AuthTokens(@NonNull List invalid) { + List tokens = new ArrayList<>(getSvr2AuthTokens()); if (tokens.removeAll(invalid)) { - putAuthTokenList(tokens); + putSvr2AuthTokens(tokens); + return true; + } + + return false; + } + + public boolean removeSvr3AuthTokens(@NonNull List invalid) { + List tokens = new ArrayList<>(getSvr3AuthTokens()); + if (tokens.removeAll(invalid)) { + putSvr3AuthTokens(tokens); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt new file mode 100644 index 0000000000..31ad5141c6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.pin + +import org.thoughtcrime.securesms.util.FeatureFlags + +object Svr3Migration { + + /** + * Whether we should read from SVR3. This is a compile-time flag because it affects what happens pre-registration. + * It only exists so that we can merge this in before SVR3 is ready server-side. This flag will be removed once we actually launch SVR3 support. + */ + const val shouldReadFromSvr3 = false + + /** + * Whether or not you should write to SVR3. If [shouldWriteToSvr2] is also enabled, you should write to SVR3 first. + */ + val shouldWriteToSvr3: Boolean + get() = shouldReadFromSvr3 && FeatureFlags.svr3MigrationPhase().let { it == 1 || it == 2 } + + /** + * Whether or not you should write to SVR2. If [shouldWriteToSvr3] is also enabled, you should write to SVR3 first. + */ + val shouldWriteToSvr2: Boolean + get() = !shouldReadFromSvr3 || FeatureFlags.svr3MigrationPhase() != 2 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt index a82448ced4..752ffa56d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob import org.thoughtcrime.securesms.jobs.StorageForcePushJob import org.thoughtcrime.securesms.jobs.StorageSyncJob import org.thoughtcrime.securesms.jobs.Svr2MirrorJob +import org.thoughtcrime.securesms.jobs.Svr3MirrorJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.megaphone.Megaphones @@ -29,6 +30,8 @@ import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.svr.SecureValueRecovery import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse +import org.whispersystems.signalservice.api.svr.SecureValueRecovery.SvrVersion +import org.whispersystems.signalservice.api.svr.Svr3Credentials import org.whispersystems.signalservice.internal.push.AuthCredentials import java.io.IOException import java.util.concurrent.TimeUnit @@ -40,12 +43,23 @@ object SvrRepository { val TAG = Log.tag(SvrRepository::class.java) private val svr2: SecureValueRecovery = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE) + private val svr3: SecureValueRecovery = AppDependencies.signalServiceAccountManager.getSecureValueRecoveryV3(AppDependencies.libsignalNetwork) /** An ordered list of SVR implementations to read from. They should be in priority order, with the most important one listed first. */ - private val readImplementations: List = listOf(svr2) + private val readImplementations: List = if (Svr3Migration.shouldReadFromSvr3) listOf(svr3, svr2) else listOf(svr2) /** An ordered list of SVR implementations to write to. They should be in priority order, with the most important one listed first. */ - private val writeImplementations: List = listOf(svr2) + private val writeImplementations: List + get() { + val implementations = mutableListOf() + if (Svr3Migration.shouldWriteToSvr3) { + implementations += svr3 + } + if (Svr3Migration.shouldWriteToSvr2) { + implementations += svr2 + } + return implementations + } /** * A lock that ensures that only one thread at a time is altering the various pieces of SVR state. @@ -72,16 +86,23 @@ object SvrRepository { operationLock.withLock { Log.i(TAG, "restoreMasterKeyPreRegistration()", true) - val operations: List RestoreResponse>> = listOf( - svr2 to { restoreMasterKeyPreRegistration(svr2, credentials.svr2, userPin) } - ) + val operations: List RestoreResponse>> = if (Svr3Migration.shouldReadFromSvr3) { + listOf( + svr3 to { restoreMasterKeyPreRegistrationFromV3(credentials.svr3, userPin) }, + svr2 to { restoreMasterKeyPreRegistrationFromV2(credentials.svr2, userPin) } + ) + } else { + listOf(svr2 to { restoreMasterKeyPreRegistrationFromV2(credentials.svr2, userPin) }) + } for ((implementation, operation) in operations) { when (val response: RestoreResponse = operation()) { is RestoreResponse.Success -> { Log.i(TAG, "[restoreMasterKeyPreRegistration] Successfully restored master key. $implementation", true) - if (implementation == svr2) { - SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic()) + + when (implementation.svrVersion) { + SvrVersion.SVR2 -> SignalStore.svr().appendSvr2AuthTokenToList(response.authorization.asBasic()) + SvrVersion.SVR3 -> SignalStore.svr().appendSvr3AuthTokenToList(response.authorization.asBasic()) } return response.masterKey @@ -138,9 +159,12 @@ object SvrRepository { SignalStore.storageService().setNeedsAccountRestore(false) SignalStore.pinValues().keyboardType = pinKeyboardType SignalStore.storageService().setNeedsAccountRestore(false) - if (implementation == svr2) { - SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic()) + + when (implementation.svrVersion) { + SvrVersion.SVR2 -> SignalStore.svr().appendSvr2AuthTokenToList(response.authorization.asBasic()) + SvrVersion.SVR3 -> SignalStore.svr().appendSvr3AuthTokenToList(response.authorization.asBasic()) } + AppDependencies.jobManager.add(ResetSvrGuessCountJob()) stopwatch.split("metadata") @@ -154,11 +178,14 @@ object SvrRepository { .enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10)) stopwatch.split("contact-restore") - // TODO [svr3] - if (implementation != svr2) { + if (implementation.svrVersion != SvrVersion.SVR2 && Svr3Migration.shouldWriteToSvr2) { AppDependencies.jobManager.add(Svr2MirrorJob()) } + if (implementation.svrVersion != SvrVersion.SVR3 && Svr3Migration.shouldWriteToSvr3) { + AppDependencies.jobManager.add(Svr3MirrorJob()) + } + stopwatch.stop(TAG) return response @@ -200,7 +227,9 @@ object SvrRepository { return operationLock.withLock { val masterKey: MasterKey = SignalStore.svr().getOrCreateMasterKey() - val responses: List = writeImplementations + val writeTargets = writeImplementations + + val responses: List = writeTargets .map { it.setPin(userPin, masterKey) } .map { it.execute() } @@ -226,7 +255,14 @@ object SvrRepository { SignalStore.svr().setMasterKey(masterKey, userPin) SignalStore.svr().isPinForgottenOrSkipped = false - SignalStore.svr().appendAuthTokenToList(overallResponse.authorization.asBasic()) + responses + .filterIsInstance() + .forEach { + when (it.svrVersion) { + SvrVersion.SVR2 -> SignalStore.svr().appendSvr2AuthTokenToList(it.authorization.asBasic()) + SvrVersion.SVR3 -> SignalStore.svr().appendSvr3AuthTokenToList(it.authorization.asBasic()) + } + } SignalStore.pinValues().keyboardType = keyboardType SignalStore.pinValues().resetPinReminders() @@ -348,8 +384,23 @@ object SvrRepository { @Throws(IOException::class) fun refreshAndStoreAuthorization() { try { - val credentials: AuthCredentials = svr2.authorization() - backupSvrCredentials(credentials) + var newToken = if (Svr3Migration.shouldWriteToSvr3) { + val credentials: AuthCredentials = svr3.authorization() + SignalStore.svr().appendSvr3AuthTokenToList(credentials.asBasic()) + } else { + false + } + + newToken = newToken || if (Svr3Migration.shouldWriteToSvr2) { + val credentials: AuthCredentials = svr2.authorization() + SignalStore.svr().appendSvr2AuthTokenToList(credentials.asBasic()) + } else { + false + } + + if (newToken && SignalStore.svr().hasPin()) { + BackupManager(AppDependencies.application).dataChanged() + } } catch (e: Throwable) { if (e is IOException) { throw e @@ -361,11 +412,21 @@ object SvrRepository { @WorkerThread @VisibleForTesting - fun restoreMasterKeyPreRegistration(svr: SecureValueRecovery, credentials: AuthCredentials?, userPin: String): RestoreResponse { + fun restoreMasterKeyPreRegistrationFromV2(credentials: AuthCredentials?, userPin: String): RestoreResponse { return if (credentials == null) { RestoreResponse.Missing } else { - svr.restoreDataPreRegistration(credentials, userPin) + svr2.restoreDataPreRegistration(credentials, shareSet = null, userPin) + } + } + + @WorkerThread + @VisibleForTesting + fun restoreMasterKeyPreRegistrationFromV3(credentials: Svr3Credentials?, userPin: String): RestoreResponse { + return if (credentials?.shareSet == null) { + RestoreResponse.Missing + } else { + svr3.restoreDataPreRegistration(credentials.authCredentials, credentials.shareSet, userPin) } } @@ -395,14 +456,6 @@ object SvrRepository { } } - private fun backupSvrCredentials(credentials: AuthCredentials) { - val tokenIsNew = SignalStore.svr().appendAuthTokenToList(credentials.asBasic()) - - if (tokenIsNew && SignalStore.svr().hasPin()) { - BackupManager(AppDependencies.application).dataChanged() - } - } - private val hasNoRegistrationLock: Boolean get() { return !SignalStore.svr().isRegistrationLockEnabled && diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index 5308c136a3..9972f3e671 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -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(); } }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponseProcessor.kt index 43ffaae3ec..44be9a2311 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponseProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyResponseProcessor.kt @@ -21,7 +21,7 @@ sealed class VerifyResponseProcessor(response: ServiceResponse) 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) */ class VerifyResponseWithoutKbs(response: ServiceResponse) : VerifyResponseProcessor(response) { override fun isRegistrationLockPresentAndSvrExhausted(): Boolean { - return registrationLock() && getLockedException().svr1Credentials == null && getLockedException().svr2Credentials == null + return registrationLock() && getLockedException().svr2Credentials == null } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt index c2866e5a58..b53619450a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/RegistrationRepository.kt @@ -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 = 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.toSvrCredentials(): List { + return this .asSequence() .filterNotNull() - .take(10) - .map { - it.replace("Basic ", "").trim() - } - .mapNotNull { + .take(10) + .map { it.replace("Basic ", "").trim() } + .mapNotNull { try { Base64.decode(it) } catch (e: IOException) { @@ -525,9 +546,7 @@ object RegistrationRepository { null } } - .map { - String(it, StandardCharsets.ISO_8859_1) - } + .map { String(it, StandardCharsets.ISO_8859_1) } .toList() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt index 10acf03c8b..2a8b6c1873 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/BackupAuthCheckResult.kt @@ -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): BackupAuthCheckResult { + fun fromV2(networkResult: NetworkResult): 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): 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt index a00486646f..466eeef6bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/RegisterAccountResult.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt index aaf5268c28..fafc415a4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/data/network/VerificationCodeRequestResult.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt index 4c187e515c..7685788ac8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2State.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt index f7fdff2d5a..47925300ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2ViewModel.kt @@ -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 + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index a52ba809e8..dbcc3494ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -353,7 +353,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { } private Single checkForValidSvrAuthCredentials() { - final List svrAuthTokenList = SignalStore.svr().getAuthTokenList(); + final List svrAuthTokenList = SignalStore.svr().getSvr2AuthTokens(); List 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."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/SvrAuthCredentialSet.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/SvrAuthCredentialSet.kt index 74af655131..fda7aab653 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/SvrAuthCredentialSet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/SvrAuthCredentialSet.kt @@ -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) + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index ec73a1a37d..d5293a2059 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -130,6 +130,7 @@ public final class FeatureFlags { private static final String LIBSIGNAL_WEB_SOCKET_SHADOW_PCT = "android.libsignalWebSocketShadowingPercentage"; private static final String DELETE_SYNC_SEND_RECEIVE = "android.deleteSyncSendReceive"; private static final String LINKED_DEVICES_V2 = "android.linkedDevices.v2"; + private static final String SVR3_MIGRATION_PHASE = "global.svr3.phase"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -208,7 +209,8 @@ public final class FeatureFlags { CAMERAX_CUSTOM_CONTROLLER, LIBSIGNAL_WEB_SOCKET_ENABLED, LIBSIGNAL_WEB_SOCKET_SHADOW_PCT, - DELETE_SYNC_SEND_RECEIVE + DELETE_SYNC_SEND_RECEIVE, + SVR3_MIGRATION_PHASE ); @VisibleForTesting @@ -283,7 +285,8 @@ public final class FeatureFlags { RX_MESSAGE_SEND, LINKED_DEVICE_LIFESPAN_SECONDS, CAMERAX_CUSTOM_CONTROLLER, - DELETE_SYNC_SEND_RECEIVE + DELETE_SYNC_SEND_RECEIVE, + SVR3_MIGRATION_PHASE ); /** @@ -308,6 +311,8 @@ public final class FeatureFlags { */ private static final Map FLAG_CHANGE_LISTENERS = new HashMap() {{ put(MESSAGE_PROCESSOR_ALARM_INTERVAL, change -> RoutineMessageFetchReceiver.startOrUpdateAlarm(AppDependencies.getApplication())); + // TODO [svr3] we need to know what it changed from and to so we can enqueue for 0 -> 1 +// put(SVR3_MIGRATION_PHASE, change -> if (change)); }}; private static final Map REMOTE_VALUES = new TreeMap<>(); @@ -756,6 +761,11 @@ public final class FeatureFlags { return getBoolean(LINKED_DEVICES_V2, false); } + /** Which phase we're in for the SVR3 migration */ + public static int svr3MigrationPhase() { + return getInteger(SVR3_MIGRATION_PHASE, 0); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/protowire/ExternalBackups.proto b/app/src/main/protowire/ExternalBackups.proto index ce6cbb96b1..bf4db241c5 100644 --- a/app/src/main/protowire/ExternalBackups.proto +++ b/app/src/main/protowire/ExternalBackups.proto @@ -11,5 +11,6 @@ package signal; option java_package = "org.thoughtcrime.securesms.absbackup.protos"; message SvrAuthToken { - repeated string tokens = 1; + repeated string svr2Tokens = 1; + repeated string svr3Tokens = 2; } \ No newline at end of file diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index 529b5318dd..4d6c97feda 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -99,3 +99,7 @@ message DeleteSyncJobData { repeated ThreadDelete threadDeletes = 2; repeated ThreadDelete localOnlyThreadDeletes = 3; } + +message Svr3MirrorJobData { + optional string serializedChangeSession = 1; +} \ No newline at end of file diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt index 2063dfd192..8b5940a957 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -129,7 +129,7 @@ sealed class NetworkResult( /** * Will perform an operation if the result at this point in the chain is successful. Note that it runs if the chain is _currently_ successful. It does not - * depend on anything futher down the chain. + * depend on anything further down the chain. * * ```kotlin * val networkResult: NetworkResult = NetworkResult diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index d9eeaaa126..6f342fcf57 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -57,6 +57,7 @@ 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.svr.SecureValueRecoveryV3; +import org.whispersystems.signalservice.api.svr.SvrApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -64,7 +65,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher; import org.whispersystems.signalservice.internal.push.AuthCredentials; import org.whispersystems.signalservice.internal.push.BackupAuthCheckRequest; -import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse; +import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse; import org.whispersystems.signalservice.internal.push.CdsiAuthResponse; import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts; import org.whispersystems.signalservice.internal.push.PaymentAddress; @@ -183,8 +184,8 @@ public class SignalServiceAccountManager { return new SecureValueRecoveryV2(configuration, mrEnclave, pushServiceSocket); } - public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network, SecureValueRecoveryV3.ShareSetStorage storage) { - return new SecureValueRecoveryV3(network, pushServiceSocket, storage); + public SecureValueRecoveryV3 getSecureValueRecoveryV3(Network network) { + return new SecureValueRecoveryV3(network, pushServiceSocket); } public WhoAmIResponse getWhoAmI() throws IOException { @@ -205,9 +206,9 @@ public class SignalServiceAccountManager { } } - public Single> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List usernamePasswords) { + public Single> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List usernamePasswords) { - return pushServiceSocket.checkBackupAuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupAuthCheckResponse.class)); + return pushServiceSocket.checkSvr2AuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupV2AuthCheckResponse.class)); } /** @@ -879,6 +880,10 @@ public class SignalServiceAccountManager { return new RegistrationApi(pushServiceSocket); } + public SvrApi getSvrApi() { + return new SvrApi(pushServiceSocket); + } + public AuthCredentials getPaymentsAuthorization() throws IOException { return pushServiceSocket.getPaymentsAuthorization(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 78b28ae0fd..673f9485e7 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -9,7 +9,8 @@ import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.account.AccountAttributes import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest import org.whispersystems.signalservice.api.account.PreKeyCollection -import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse +import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse +import org.whispersystems.signalservice.internal.push.BackupV3AuthCheckResponse import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse @@ -103,13 +104,24 @@ class RegistrationApi( } /** - * Retrieves an SVR auth credential that corresponds with the supplied username and password. + * Validates the provided SVR2 auth credentials, returning information on their usability. * * `POST /v2/backup/auth/check` */ - fun getSvrAuthCredential(e164: String, usernamePasswords: List): NetworkResult { + fun validateSvr2AuthCredential(e164: String, usernamePasswords: List): NetworkResult { return NetworkResult.fromFetch { - pushServiceSocket.checkBackupAuthCredentials(e164, usernamePasswords) + pushServiceSocket.checkSvr2AuthCredentials(e164, usernamePasswords) + } + } + + /** + * Validates the provided SVR3 auth credentials, returning information on their usability. + * + * `POST /v3/backup/auth/check` + */ + fun validateSvr3AuthCredential(e164: String, usernamePasswords: List): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.checkSvr3AuthCredentials(e164, usernamePasswords) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecovery.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecovery.kt index 4235655cc4..2386cc2191 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecovery.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecovery.kt @@ -11,6 +11,12 @@ import java.io.IOException import kotlin.jvm.Throws interface SecureValueRecovery { + + /** + * Which version of SVR this is running against. + */ + val svrVersion: SvrVersion + /** * Begins a PIN change. * @@ -38,12 +44,14 @@ interface SecureValueRecovery { * 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] + * If the user is already registered, use [restoreDataPostRegistration]. + * + * @param shareSet Only used for SVR3, where the value is required. For SVR2, this should be null. */ - fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse + fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): 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] + * 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): RestoreResponse @@ -66,7 +74,7 @@ interface SecureValueRecovery { /** Response for setting a PIN. */ sealed class BackupResponse { /** Operation completed successfully. */ - data class Success(val masterKey: MasterKey, val authorization: AuthCredentials) : BackupResponse() + data class Success(val masterKey: MasterKey, val authorization: AuthCredentials, val svrVersion: SvrVersion) : 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() @@ -122,4 +130,9 @@ interface SecureValueRecovery { /** Exception indicating that we received a response from the service that our request was invalid. */ class InvalidRequestException(message: String) : Exception(message) + + enum class SvrVersion { + SVR2, + SVR3 + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt index 77fd500966..2aacf44bf7 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV2.kt @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.DeleteRespon 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.api.svr.SecureValueRecovery.SvrVersion import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -43,6 +44,8 @@ class SecureValueRecoveryV2( private val TAG = SecureValueRecoveryV2::class.java.simpleName } + override val svrVersion: SvrVersion = SvrVersion.SVR2 + override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession { return Svr2PinChangeSession(userPin, masterKey) } @@ -57,7 +60,7 @@ class SecureValueRecoveryV2( } } - override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse { + override fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse { return restoreData({ authorization }, userPin) } @@ -89,7 +92,7 @@ class SecureValueRecoveryV2( @Throws(IOException::class) override fun authorization(): AuthCredentials { - return pushServiceSocket.svrAuthorization + return pushServiceSocket.svr2Authorization } override fun toString(): String { @@ -249,7 +252,7 @@ class SecureValueRecoveryV2( .let { response -> when (response.expose?.status) { ProtoExposeResponse.Status.OK -> { - BackupResponse.Success(masterKey, authorization) + BackupResponse.Success(masterKey, authorization, SvrVersion.SVR2) } ProtoExposeResponse.Status.ERROR -> { BackupResponse.ExposeFailure diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt index d095cf2b06..e10885251c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SecureValueRecoveryV3.kt @@ -5,6 +5,9 @@ package org.whispersystems.signalservice.api.svr +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize import org.signal.core.util.logging.Log import org.signal.libsignal.attest.AttestationFailedException import org.signal.libsignal.net.EnclaveAuth @@ -20,8 +23,12 @@ import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupRespon 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.api.svr.SecureValueRecovery.SvrVersion import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64 +import org.whispersystems.signalservice.internal.push.ByteArraySerializerBase64NoPadding import org.whispersystems.signalservice.internal.push.PushServiceSocket +import org.whispersystems.signalservice.internal.util.JsonUtil import java.io.IOException import java.util.concurrent.CancellationException import java.util.concurrent.ExecutionException @@ -31,28 +38,100 @@ import java.util.concurrent.ExecutionException */ class SecureValueRecoveryV3( private val network: Network, - private val pushServiceSocket: PushServiceSocket, - private val shareSetStorage: ShareSetStorage + private val pushServiceSocket: PushServiceSocket ) : SecureValueRecovery { companion object { val TAG = Log.tag(SecureValueRecoveryV3::class) } + override val svrVersion: SvrVersion = SvrVersion.SVR3 + override fun setPin(userPin: String, masterKey: MasterKey): PinChangeSession { - return Svr3PinChangeSession(userPin, masterKey) + return Svr3PinChangeSession(userPin, masterKey, null) } /** * Note: Unlike SVR2, there is no concept of "resuming", so this is equivalent to starting a new session. */ override fun resumePinChangeSession(userPin: String, masterKey: MasterKey, serializedChangeSession: String): PinChangeSession { - return Svr3PinChangeSession(userPin, masterKey) + val data: Svr3SessionData = JsonUtil.fromJson(serializedChangeSession, Svr3SessionData::class.java) + + return if (data.userPin == userPin && data.masterKey == masterKey) { + Svr3PinChangeSession(userPin, masterKey, data.shareSet) + } else { + Svr3PinChangeSession(userPin, masterKey, null) + } } - override fun restoreDataPreRegistration(authorization: AuthCredentials, userPin: String): RestoreResponse { + override fun restoreDataPreRegistration(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse { + return restoreData(authorization, shareSet, userPin) + } + + override fun restoreDataPostRegistration(userPin: String): RestoreResponse { + val authorization: Svr3Credentials = try { + pushServiceSocket.svr3Authorization + } catch (e: NonSuccessfulResponseCodeException) { + return RestoreResponse.ApplicationError(e) + } catch (e: IOException) { + return RestoreResponse.NetworkError(e) + } catch (e: Exception) { + return RestoreResponse.ApplicationError(e) + } + + return restoreData(authorization.toAuthCredential(), authorization.shareSet, userPin) + } + + /** + * There's no concept of "deleting" data with SVR3. + */ + override fun deleteData(): DeleteResponse { + val authorization: Svr3Credentials = try { + pushServiceSocket.svr3Authorization + } catch (e: NonSuccessfulResponseCodeException) { + return DeleteResponse.ApplicationError(e) + } catch (e: IOException) { + return DeleteResponse.NetworkError(e) + } catch (e: Exception) { + return DeleteResponse.ApplicationError(e) + } + + val enclaveAuth = EnclaveAuth(authorization.username, authorization.password) + + return try { + network.svr3().remove(enclaveAuth).get() + DeleteResponse.Success + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> DeleteResponse.NetworkError(cause) + is AttestationFailedException -> DeleteResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> DeleteResponse.ApplicationError(cause) + is IOException -> DeleteResponse.NetworkError(cause) + else -> DeleteResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + DeleteResponse.ApplicationError(e) + } catch (e: CancellationException) { + DeleteResponse.ApplicationError(e) + } + } + + @Throws(IOException::class) + override fun authorization(): AuthCredentials { + return pushServiceSocket.svr3Authorization.toAuthCredential() + } + + override fun toString(): String { + return "SVR3" + } + + private fun restoreData(authorization: AuthCredentials, shareSet: ByteArray?, userPin: String): RestoreResponse { + if (shareSet == null) { + Log.w(TAG, "No share set provided! Assuming no data to restore.") + return RestoreResponse.Missing + } + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) - val shareSet = shareSetStorage.read() ?: return RestoreResponse.ApplicationError(IllegalStateException("No share set found!")) val enclaveAuth = EnclaveAuth(authorization.username(), authorization.password()) return try { @@ -76,40 +155,22 @@ class SecureValueRecoveryV3( } } - override fun restoreDataPostRegistration(userPin: String): RestoreResponse { - val authorization: AuthCredentials = try { - pushServiceSocket.svrAuthorization - } catch (e: NonSuccessfulResponseCodeException) { - return RestoreResponse.ApplicationError(e) - } catch (e: IOException) { - return RestoreResponse.NetworkError(e) - } catch (e: Exception) { - return RestoreResponse.ApplicationError(e) - } - - return restoreDataPreRegistration(authorization, userPin) - } - - /** - * There's no concept of "deleting" data with SVR3. - */ - override fun deleteData(): DeleteResponse { - return DeleteResponse.Success - } - - @Throws(IOException::class) - override fun authorization(): AuthCredentials { - return pushServiceSocket.svrAuthorization + private fun Svr3Credentials.toAuthCredential(): AuthCredentials { + return AuthCredentials.create(username, password) } inner class Svr3PinChangeSession( private val userPin: String, - private val masterKey: MasterKey + private val masterKey: MasterKey, + private var shareSet: ByteArray? ) : PinChangeSession { + + /** + * Performs the PIN change operation. This is safe to call repeatedly if you get back a retryable error. + */ override fun execute(): BackupResponse { - val normalizedPin: String = PinHashUtil.normalizeToString(userPin) - val rawAuth = try { - pushServiceSocket.svrAuthorization + val rawAuth: Svr3Credentials = try { + pushServiceSocket.svr3Authorization } catch (e: NonSuccessfulResponseCodeException) { return BackupResponse.ApplicationError(e) } catch (e: IOException) { @@ -118,38 +179,56 @@ class SecureValueRecoveryV3( return BackupResponse.ApplicationError(e) } - val enclaveAuth = EnclaveAuth(rawAuth.username(), rawAuth.password()) + if (shareSet == null) { + val normalizedPin: String = PinHashUtil.normalizeToString(userPin) + val enclaveAuth = EnclaveAuth(rawAuth.username, rawAuth.password) + + try { + shareSet = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get() + } catch (e: ExecutionException) { + when (val cause = e.cause) { + is NetworkException -> BackupResponse.NetworkError(cause) + is AttestationFailedException -> BackupResponse.ApplicationError(cause) + is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause) + is IOException -> BackupResponse.NetworkError(cause) + else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) + } + } catch (e: InterruptedException) { + BackupResponse.ApplicationError(e) + } catch (e: CancellationException) { + BackupResponse.ApplicationError(e) + } + } return try { - val result = network.svr3().backup(masterKey.serialize(), normalizedPin, 10, enclaveAuth).get() - shareSetStorage.write(result) - BackupResponse.Success(masterKey, rawAuth) - } catch (e: ExecutionException) { - when (val cause = e.cause) { - is NetworkException -> BackupResponse.NetworkError(cause) - is AttestationFailedException -> BackupResponse.ApplicationError(cause) - is SgxCommunicationFailureException -> BackupResponse.ApplicationError(cause) - is IOException -> BackupResponse.NetworkError(cause) - else -> BackupResponse.ApplicationError(cause ?: RuntimeException("Unknown!")) - } - } catch (e: InterruptedException) { - BackupResponse.ApplicationError(e) - } catch (e: CancellationException) { + pushServiceSocket.setShareSet(shareSet) + BackupResponse.Success(masterKey, pushServiceSocket.svr3Authorization.toAuthCredential(), SvrVersion.SVR3) + } catch (e: NonSuccessfulResponseCodeException) { BackupResponse.ApplicationError(e) + } catch (e: IOException) { + BackupResponse.NetworkError(e) + } catch (e: Exception) { + return BackupResponse.ApplicationError(e) } } override fun serialize(): String { - // There is no "resuming" SVR3, so we don't need to serialize anything - return "" + return JsonUtil.toJson(Svr3SessionData(userPin, masterKey, shareSet)) } } - /** - * An interface to allow reading and writing the "share set" to persistent storage. - */ - interface ShareSetStorage { - fun write(data: ByteArray) - fun read(): ByteArray? - } + class Svr3SessionData( + @JsonProperty + val userPin: String, + + @JsonProperty + @JsonSerialize(using = JsonUtil.MasterKeySerializer::class) + @JsonDeserialize(using = JsonUtil.MasterKeyDeserializer::class) + val masterKey: MasterKey, + + @JsonProperty + @JsonSerialize(using = ByteArraySerializerBase64NoPadding::class) + @JsonDeserialize(using = ByteArrayDeserializerBase64::class) + val shareSet: ByteArray? + ) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SetShareSetRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SetShareSetRequest.kt new file mode 100644 index 0000000000..b4925ebb79 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SetShareSetRequest.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.svr + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import org.whispersystems.signalservice.internal.push.ByteArraySerializerBase64NoPadding + +/** + * Request body for setting a share-set on the service. + */ +class SetShareSetRequest( + @JsonProperty + @JsonSerialize(using = ByteArraySerializerBase64NoPadding::class) + val shareSet: ByteArray +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr3Credentials.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr3Credentials.kt new file mode 100644 index 0000000000..4cafdd2be0 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/Svr3Credentials.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.svr + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.ByteArrayDeserializerBase64 + +/** + * Response object when fetching SVR3 auth credentials. We also use it elsewhere as a convenient container + * for the (username, password, shareset) tuple. + */ +class Svr3Credentials( + @JsonProperty + val username: String, + + @JsonProperty + val password: String, + + @JsonProperty + @JsonDeserialize(using = ByteArrayDeserializerBase64::class) + val shareSet: ByteArray? +) { + val authCredentials: AuthCredentials + get() = AuthCredentials.create(username, password) +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SvrApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SvrApi.kt new file mode 100644 index 0000000000..20d162144f --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/svr/SvrApi.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.svr + +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.internal.push.PushServiceSocket + +class SvrApi(private val pushServiceSocket: PushServiceSocket) { + + companion object { + @JvmStatic + fun create(pushServiceSocket: PushServiceSocket): SvrApi { + return SvrApi(pushServiceSocket) + } + } + + /** + * Store the latest share-set on the service. The share-set is a piece of data generated in the course of setting a PIN on SVR3 that needs to be + */ + fun setShareSet(shareSet: ByteArray): NetworkResult { + return NetworkResult.fromFetch { + pushServiceSocket.setShareSet(shareSet) + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckProcessor.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckProcessor.kt new file mode 100644 index 0000000000..0eba27d6ee --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckProcessor.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor + +/** + * Processes a response from the verify stored KBS auth credentials request. + */ +class BackupAuthCheckProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { + fun getInvalid(): List { + return response.result.map { it.invalid }.orElse(emptyList()) + } + + fun hasValidSvr2AuthCredential(): Boolean { + return response.result.map { it.match }.orElse(null) != null + } + + fun requireSvr2AuthCredential(): AuthCredentials { + return response.result.get().match!! + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt index dabb05ce4a..52a2d177c6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt @@ -1,68 +1,14 @@ package org.whispersystems.signalservice.internal.push -import com.fasterxml.jackson.annotation.JsonCreator -import okio.ByteString.Companion.encode -import org.whispersystems.signalservice.internal.ServiceResponse -import org.whispersystems.signalservice.internal.ServiceResponseProcessor -import java.io.IOException -import java.nio.charset.StandardCharsets +import com.fasterxml.jackson.annotation.JsonProperty /** * Request body JSON for verifying stored KBS auth credentials. */ -@Suppress("unused") -class BackupAuthCheckRequest @JsonCreator constructor( +class BackupAuthCheckRequest( + @JsonProperty val number: String?, + + @JsonProperty val passwords: List ) - -/** - * Verify KBS auth credentials JSON response. - */ -data class BackupAuthCheckResponse @JsonCreator constructor( - private val matches: Map> -) { - private val actualMatches = matches["matches"] ?: emptyMap() - - val match: AuthCredentials? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toAuthCredential() - val invalid: List = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() } - - /** Server expects and returns values as : but we prefer the full encoded Basic auth header format */ - private fun String.toBasic(): String { - return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}" - } - - private fun String.toAuthCredential(): AuthCredentials { - val firstColonIndex = this.indexOf(":") - - if (firstColonIndex < 0) { - throw IOException("Invalid credential returned!") - } - - val username = this.substring(0, firstColonIndex) - val password = this.substring(firstColonIndex + 1) - - return AuthCredentials.create(username, password) - } - - fun merge(other: BackupAuthCheckResponse): BackupAuthCheckResponse { - return BackupAuthCheckResponse(this.matches + other.matches) - } -} - -/** - * Processes a response from the verify stored KBS auth credentials request. - */ -class BackupAuthCheckProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { - fun getInvalid(): List { - return response.result.map { it.invalid }.orElse(emptyList()) - } - - fun hasValidSvr2AuthCredential(): Boolean { - return response.result.map { it.match }.orElse(null) != null - } - - fun requireSvr2AuthCredential(): AuthCredentials { - return response.result.get().match!! - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV2AuthCheckResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV2AuthCheckResponse.kt new file mode 100644 index 0000000000..894553725f --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV2AuthCheckResponse.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.annotation.JsonCreator +import okio.ByteString.Companion.encode +import java.io.IOException +import java.nio.charset.StandardCharsets + +/** + * Verify KBS auth credentials JSON response. + */ +data class BackupV2AuthCheckResponse @JsonCreator constructor( + private val matches: Map> +) { + private val actualMatches = matches["matches"] ?: emptyMap() + + val match: AuthCredentials? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toAuthCredential() + val invalid: List = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() } + + /** Server expects and returns values as : but we prefer the full encoded Basic auth header format */ + private fun String.toBasic(): String { + return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}" + } + + private fun String.toAuthCredential(): AuthCredentials { + val firstColonIndex = this.indexOf(":") + + if (firstColonIndex < 0) { + throw IOException("Invalid credential returned!") + } + + val username = this.substring(0, firstColonIndex) + val password = this.substring(firstColonIndex + 1) + + return AuthCredentials.create(username, password) + } + + fun merge(other: BackupV2AuthCheckResponse): BackupV2AuthCheckResponse { + return BackupV2AuthCheckResponse(this.matches + other.matches) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV3AuthCheckResponse.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV3AuthCheckResponse.kt new file mode 100644 index 0000000000..1c4e706831 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BackupV3AuthCheckResponse.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import okio.ByteString.Companion.encode +import org.whispersystems.signalservice.api.svr.Svr3Credentials +import java.io.IOException +import java.nio.charset.StandardCharsets + +/** + * Response when verifying whether we have valid SVR3 credentials. + * + * We use a map here because the keys are dynamic. The response looks like this: + * + * ```json + * { + * "matches": { + * "username:password": { + * "status": "match", + * "shareSet": "" + * }, + * ... + * } + * } + */ +class BackupV3AuthCheckResponse( + @JsonProperty + private val matches: Map +) { + + /** A response that contains a valid SVR3 auth credential if one is present, else null. */ + val match: Svr3Credentials? = matches.entries.firstOrNull { it.value.isMatch }?.let { + val credential = it.key.toAuthCredential() + Svr3Credentials(credential.username(), credential.password(), it.value.shareSet) + } + + /** A list of credentials that are invalid, in basic-auth format. */ + val invalid: List = matches.filterValues { it.isInvalid }.keys.map { it.toBasic() } + + private fun String.toAuthCredential(): AuthCredentials { + val firstColonIndex = this.indexOf(":") + + if (firstColonIndex < 0) { + throw IOException("Invalid credential returned!") + } + + val username = this.substring(0, firstColonIndex) + val password = this.substring(firstColonIndex + 1) + + return AuthCredentials.create(username, password) + } + + /** Server expects and returns values as : but we prefer the full encoded Basic auth header format */ + private fun String.toBasic(): String { + return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}" + } + + class MatchData( + @JsonProperty + val status: String, + + @JsonProperty + @JsonDeserialize(using = ByteArrayDeserializerBase64::class) + val shareSet: ByteArray + ) { + val isMatch = status == "match" + val isInvalid = status == "invalid" + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java index eb61e64eb9..53ad86a2c1 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/LockedException.java @@ -2,20 +2,21 @@ package org.whispersystems.signalservice.internal.push; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.svr.Svr3Credentials; public final class LockedException extends NonSuccessfulResponseCodeException { private final int length; private final long timeRemaining; - private final AuthCredentials svr1Credentials; private final AuthCredentials svr2Credentials; + private final Svr3Credentials svr3Credentials; - public LockedException(int length, long timeRemaining, AuthCredentials svr1Credentials, AuthCredentials svr2Credentials) { + public LockedException(int length, long timeRemaining, AuthCredentials svr2Credentials, Svr3Credentials svr3Credentials) { super(423); this.length = length; this.timeRemaining = timeRemaining; - this.svr1Credentials = svr1Credentials; this.svr2Credentials = svr2Credentials; + this.svr3Credentials = svr3Credentials; } public int getLength() { @@ -26,11 +27,11 @@ public final class LockedException extends NonSuccessfulResponseCodeException { return timeRemaining; } - public AuthCredentials getSvr1Credentials() { - return svr1Credentials; - } - public AuthCredentials getSvr2Credentials() { return svr2Credentials; } + + public Svr3Credentials getSvr3Credentials() { + return svr3Credentials; + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index bece777efd..10b1114326 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -117,6 +117,8 @@ import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIn import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse; import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse; import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret; +import org.whispersystems.signalservice.api.svr.SetShareSetRequest; +import org.whispersystems.signalservice.api.svr.Svr3Credentials; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Tls12SocketFactory; import org.whispersystems.signalservice.api.util.TlsProxySocketFactory; @@ -306,11 +308,13 @@ public class PushServiceSocket { private static final String REGISTRATION_PATH = "/v1/registration"; private static final String CDSI_AUTH = "/v2/directory/auth"; - private static final String SVR_AUTH = "/v2/backup/auth"; + private static final String SVR2_AUTH = "/v2/backup/auth"; + private static final String SVR3_AUTH = "/v3/backup/auth"; private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; - private static final String BACKUP_AUTH_CHECK = "/v2/backup/auth/check"; + private static final String BACKUP_AUTH_CHECK_V2 = "/v2/backup/auth/check"; + private static final String BACKUP_AUTH_CHECK_V3 = "/v3/backup/auth/check"; private static final String ARCHIVE_CREDENTIALS = "/v1/archives/auth?redemptionStartSeconds=%d&redemptionEndSeconds=%d"; private static final String ARCHIVE_READ_CREDENTIALS = "/v1/archives/auth/read?cdn=%d"; @@ -325,6 +329,8 @@ public class PushServiceSocket { private static final String ARCHIVE_MEDIA_DELETE = "/v1/archives/media/delete"; private static final String ARCHIVE_MEDIA_DOWNLOAD_PATH = "backups/%s/%s/%s"; + private static final String SET_SHARE_SET_PATH = "/v3/backup/share-set"; + private static final String CALL_LINK_CREATION_AUTH = "/v1/call-link/create-auth"; private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; @@ -487,13 +493,18 @@ public class PushServiceSocket { return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); } - public AuthCredentials getSvrAuthorization() throws IOException { - String body = makeServiceRequest(SVR_AUTH, "GET", null); + public AuthCredentials getSvr2Authorization() throws IOException { + String body = makeServiceRequest(SVR2_AUTH, "GET", null); AuthCredentials credentials = JsonUtil.fromJsonResponse(body, AuthCredentials.class); return credentials; } + public Svr3Credentials getSvr3Authorization() throws IOException { + String body = makeServiceRequest(SVR3_AUTH, "GET", null); + return JsonUtil.fromJsonResponse(body, Svr3Credentials.class); + } + public ArchiveServiceCredentialsResponse getArchiveCredentials(long currentTime) throws IOException { long secondsRoundedToNearestDay = TimeUnit.DAYS.toSeconds(TimeUnit.MILLISECONDS.toDays(currentTime)); long endTimeInSeconds = secondsRoundedToNearestDay + TimeUnit.DAYS.toSeconds(7); @@ -609,6 +620,11 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, GetArchiveCdnCredentialsResponse.class); } + public void setShareSet(byte[] shareSet) throws IOException { + SetShareSetRequest request = new SetShareSetRequest(shareSet); + makeServiceRequest(SET_SHARE_SET_PATH, "PUT", JsonUtil.toJson(request)); + } + public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) throws IOException { @@ -1127,11 +1143,11 @@ public class PushServiceSocket { .onErrorReturn(ServiceResponse::forUnknownError); } - public Single> checkBackupAuthCredentials(@Nonnull BackupAuthCheckRequest request, - @Nonnull ResponseMapper responseMapper) + public Single> checkSvr2AuthCredentials(@Nonnull BackupAuthCheckRequest request, + @Nonnull ResponseMapper responseMapper) { - Single> requestSingle = Single.fromCallable(() -> { - try (Response response = getServiceConnection(BACKUP_AUTH_CHECK, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) { + Single> requestSingle = Single.fromCallable(() -> { + try (Response response = getServiceConnection(BACKUP_AUTH_CHECK_V2, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) { String body = response.body() != null ? readBodyString(response.body()): ""; return responseMapper.map(response.code(), body, response::header, false); } @@ -1143,21 +1159,14 @@ public class PushServiceSocket { .onErrorReturn(ServiceResponse::forUnknownError); } - public BackupAuthCheckResponse checkBackupAuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { - String response = makeServiceRequest(BACKUP_AUTH_CHECK, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); - return JsonUtil.fromJson(response, BackupAuthCheckResponse.class); + public BackupV2AuthCheckResponse checkSvr2AuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { + String response = makeServiceRequest(BACKUP_AUTH_CHECK_V2, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + return JsonUtil.fromJson(response, BackupV2AuthCheckResponse.class); } - private Single> createBackupAuthCheckSingle(@Nonnull String path, - @Nonnull BackupAuthCheckRequest request, - @Nonnull ResponseMapper responseMapper) - { - return Single.fromCallable(() -> { - try (Response response = getServiceConnection(path, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) { - String body = response.body() != null ? readBodyString(response.body()): ""; - return responseMapper.map(response.code(), body, response::header, false); - } - }); + public BackupV3AuthCheckResponse checkSvr3AuthCredentials(@Nullable String number, @Nonnull List passwords) throws IOException { + String response = makeServiceRequest(BACKUP_AUTH_CHECK_V3, "POST", JsonUtil.toJson(new BackupAuthCheckRequest(number, passwords)), NO_HEADERS, UNOPINIONATED_HANDLER, Optional.empty()); + return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class); } /** @@ -2243,8 +2252,8 @@ public class PushServiceSocket { throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining, - accountLockFailure.svr1Credentials, - accountLockFailure.svr2Credentials); + accountLockFailure.svr2Credentials, + accountLockFailure.svr3Credentials); case 428: ProofRequiredResponse proofRequiredResponse = readResponseJson(response, ProofRequiredResponse.class); String retryAfterRaw = response.header("Retry-After"); @@ -2676,6 +2685,9 @@ public class PushServiceSocket { @JsonProperty public AuthCredentials svr2Credentials; + + @JsonProperty + public Svr3Credentials svr3Credentials; } private static class ConnectionHolder { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java index 03e1b8457c..d90c919720 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java @@ -121,8 +121,8 @@ public final class DefaultErrorMapper implements ErrorMapper { return new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining, - accountLockFailure.svr1Credentials, - accountLockFailure.svr2Credentials); + accountLockFailure.svr2Credentials, + accountLockFailure.svr3Credentials); case 428: ProofRequiredResponse proofRequiredResponse; try {