Add mostly-working SVR3 implementation behind flag.

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

View File

@@ -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.")
}

View File

@@ -13,6 +13,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
import org.whispersystems.signalservice.api.push.exceptions.IncorrectRegistrationRecoveryPasswordException
import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException
import org.whispersystems.signalservice.api.svr.Svr3Credentials
import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
@@ -33,7 +34,7 @@ sealed class 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)

View File

@@ -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,

View File

@@ -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 -> {

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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)
)
}
}

View File

@@ -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()
}

View File

@@ -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<Svr3MirrorJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): Svr3MirrorJob {
val jobData: Svr3MirrorJobData? = serializedData?.let { Svr3MirrorJobData.ADAPTER.decode(serializedData) }
return Svr3MirrorJob(parameters, jobData?.serializedChangeSession)
}
}
}

View File

@@ -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<String> 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<String> tokens) {
putList(SVR_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE);
public synchronized void putSvr2AuthTokens(List<String> tokens) {
putList(SVR2_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE);
setLastRefreshAuthTimestamp(System.currentTimeMillis());
}
public synchronized List<String> getAuthTokenList() {
return getList(SVR_AUTH_TOKENS, StringStringSerializer.INSTANCE);
public synchronized void putSvr3AuthTokens(List<String> tokens) {
putList(SVR3_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE);
setLastRefreshAuthTimestamp(System.currentTimeMillis());
}
public synchronized List<String> getSvr2AuthTokens() {
return getList(SVR2_AUTH_TOKENS, StringStringSerializer.INSTANCE);
}
public synchronized List<String> 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<String> tokens = getAuthTokenList();
public synchronized boolean appendSvr2AuthTokenToList(String token) {
List<String> tokens = getSvr2AuthTokens();
if (tokens.contains(token)) {
return false;
} else {
final List<String> result = Stream.concat(Stream.of(token), tokens.stream()).limit(10).collect(Collectors.toList());
putAuthTokenList(result);
putSvr2AuthTokens(result);
return true;
}
}
public boolean removeAuthTokens(@NonNull List<String> invalid) {
List<String> 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<String> tokens = getSvr3AuthTokens();
if (tokens.contains(token)) {
return false;
} else {
final List<String> result = Stream.concat(Stream.of(token), tokens.stream()).limit(10).collect(Collectors.toList());
putSvr3AuthTokens(result);
return true;
}
}
public boolean removeSvr2AuthTokens(@NonNull List<String> invalid) {
List<String> tokens = new ArrayList<>(getSvr2AuthTokens());
if (tokens.removeAll(invalid)) {
putAuthTokenList(tokens);
putSvr2AuthTokens(tokens);
return true;
}
return false;
}
public boolean removeSvr3AuthTokens(@NonNull List<String> invalid) {
List<String> tokens = new ArrayList<>(getSvr3AuthTokens());
if (tokens.removeAll(invalid)) {
putSvr3AuthTokens(tokens);
return true;
}

View File

@@ -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
}

View File

@@ -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<SecureValueRecovery> = listOf(svr2)
private val readImplementations: List<SecureValueRecovery> = 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<SecureValueRecovery> = listOf(svr2)
private val writeImplementations: List<SecureValueRecovery>
get() {
val implementations = mutableListOf<SecureValueRecovery>()
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<Pair<SecureValueRecovery, () -> RestoreResponse>> = listOf(
svr2 to { restoreMasterKeyPreRegistration(svr2, credentials.svr2, userPin) }
)
val operations: List<Pair<SecureValueRecovery, () -> 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<BackupResponse> = writeImplementations
val writeTargets = writeImplementations
val responses: List<BackupResponse> = 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<BackupResponse.Success>()
.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 &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
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<String, Object> 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<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);