Add basic re-reg support to regV5.

This commit is contained in:
Greyson Parrelli
2026-01-30 16:57:55 -05:00
parent 85408f2b12
commit 6416df241f
20 changed files with 361 additions and 69 deletions

View File

@@ -289,6 +289,9 @@ class RealNetworkController(
fcmToken: String?,
skipDeviceTransfer: Boolean
): RegistrationNetworkResult<RegisterAccountResponse, RegisterAccountError> = withContext(Dispatchers.IO) {
check(sessionId != null || recoveryPassword != null) { "Either sessionId or recoveryPassword must be provided" }
check(sessionId == null || recoveryPassword == null) { "Either sessionId or recoveryPassword must be provided, but not both" }
try {
val serviceAttributes = attributes.toServiceAccountAttributes()
val serviceAciPreKeys = aciPreKeys.toServicePreKeyCollection()

View File

@@ -178,7 +178,7 @@ class PinSettingsViewModel(
),
name = null,
pniRegistrationId = RegistrationPreferences.pniRegistrationId,
recoveryPassword = null
recoveryPassword = RegistrationPreferences.masterKey?.deriveRegistrationRecoveryPassword()
)
when (val result = networkController.setAccountAttributes(attributes)) {

View File

@@ -28,12 +28,6 @@ class RegistrationActivity : ComponentActivity() {
)
}
private val viewModel: RegistrationViewModel by viewModels(factoryProducer = {
RegistrationViewModel.Factory(
repository = repository
)
})
@OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -5,44 +5,24 @@
package org.signal.registration
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey
import org.signal.registration.util.AccountEntropyPoolParceler
import org.signal.registration.util.MasterKeyParceler
@Parcelize
@TypeParceler<MasterKey?, MasterKeyParceler>
@TypeParceler<AccountEntropyPool?, AepParceler>
@TypeParceler<AccountEntropyPool?, AccountEntropyPoolParceler>
data class RegistrationFlowState(
val backStack: List<RegistrationRoute> = listOf(RegistrationRoute.Welcome),
val sessionMetadata: NetworkController.SessionMetadata? = null,
val sessionE164: String? = null,
val accountEntropyPool: AccountEntropyPool? = null,
val temporaryMasterKey: MasterKey? = null,
val registrationLockProof: String? = null
val registrationLockProof: String? = null,
val preExistingRegistrationData: PreExistingRegistrationData? = null
) : Parcelable
object MasterKeyParceler : Parceler<MasterKey?> {
override fun create(parcel: Parcel): MasterKey? {
val bytes = parcel.createByteArray()
return bytes?.let { MasterKey(it) }
}
override fun MasterKey?.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this?.serialize())
}
}
object AepParceler : Parceler<AccountEntropyPool?> {
override fun create(parcel: Parcel): AccountEntropyPool? {
val aep = parcel.readString()
return aep?.let { AccountEntropyPool(it) }
}
override fun AccountEntropyPool?.write(parcel: Parcel, flags: Int) {
parcel.writeString(this?.value)
}
}

View File

@@ -108,6 +108,30 @@ class RegistrationRepository(val networkController: NetworkController, val stora
}
}
/**
* Registers a new account using a recovery password derived from the user's [MasterKey].
*
* This method:
* 1. Generates and stores all required cryptographic key material
* 2. Creates account attributes with registration IDs and capabilities
* 3. Calls the network controller to register the account
* 4. On success, saves the registration data to persistent storage
*
* @param e164 The phone number in E.164 format (used for basic auth)
* @param recoveryPassword The recovery password, derived from the user's [MasterKey], which allows us to forgo session creation.
* @param registrationLock The registration lock token derived from the master key (if unlocking a reglocked account)
* @param skipDeviceTransfer Whether to skip device transfer flow
* @return The registration result containing account information or an error
*/
suspend fun registerAccountWithRecoveryPassword(
e164: String,
recoveryPassword: String,
registrationLock: String? = null,
skipDeviceTransfer: Boolean = true
): RegistrationNetworkResult<Pair<RegisterAccountResponse, KeyMaterial>, RegisterAccountError> = withContext(Dispatchers.IO) {
registerAccount(e164, sessionId = null, recoveryPassword, registrationLock, skipDeviceTransfer)
}
/**
* Registers a new account after successful phone number verification.
*
@@ -123,12 +147,41 @@ class RegistrationRepository(val networkController: NetworkController, val stora
* @param skipDeviceTransfer Whether to skip device transfer flow
* @return The registration result containing account information or an error
*/
suspend fun registerAccount(
suspend fun registerAccountWithSession(
e164: String,
sessionId: String,
registrationLock: String? = null,
skipDeviceTransfer: Boolean = true
): RegistrationNetworkResult<Pair<RegisterAccountResponse, KeyMaterial>, RegisterAccountError> = withContext(Dispatchers.IO) {
registerAccount(e164, sessionId, recoveryPassword = null, registrationLock, skipDeviceTransfer)
}
/**
* Registers a new account.
*
* This method:
* 1. Generates and stores all required cryptographic key material
* 2. Creates account attributes with registration IDs and capabilities
* 3. Calls the network controller to register the account
* 4. On success, saves the registration data to persistent storage
*
* @param e164 The phone number in E.164 format (used for basic auth)
* @param sessionId The verified session ID from phone number verification. Must provide if you're not using [recoveryPassword].
* @param recoveryPassword The recovery password, derived from the user's [MasterKey], which allows us to forgo session creation. Must provide if you're not using [sessionId].
* @param registrationLock The registration lock token derived from the master key (if unlocking a reglocked account)
* @param skipDeviceTransfer Whether to skip device transfer flow
* @return The registration result containing account information or an error
*/
private suspend fun registerAccount(
e164: String,
sessionId: String?,
recoveryPassword: String?,
registrationLock: String? = null,
skipDeviceTransfer: Boolean = true
): RegistrationNetworkResult<Pair<RegisterAccountResponse, KeyMaterial>, RegisterAccountError> = withContext(Dispatchers.IO) {
check(sessionId != null || recoveryPassword != null) { "Either sessionId or recoveryPassword must be provided" }
check(sessionId == null || recoveryPassword == null) { "Either sessionId or recoveryPassword must be provided, but not both" }
val keyMaterial = storageController.generateAndStoreKeyMaterial()
val fcmToken = networkController.getFcmToken()
@@ -150,7 +203,7 @@ class RegistrationRepository(val networkController: NetworkController, val stora
),
name = null,
pniRegistrationId = keyMaterial.pniRegistrationId,
recoveryPassword = null
recoveryPassword = keyMaterial.accountEntropyPool.deriveMasterKey().deriveRegistrationRecoveryPassword()
)
val aciPreKeys = PreKeyCollection(
@@ -169,7 +222,7 @@ class RegistrationRepository(val networkController: NetworkController, val stora
e164 = e164,
password = keyMaterial.servicePassword,
sessionId = sessionId,
recoveryPassword = null,
recoveryPassword = recoveryPassword,
attributes = accountAttributes,
aciPreKeys = aciPreKeys,
pniPreKeys = pniPreKeys,
@@ -205,4 +258,8 @@ class RegistrationRepository(val networkController: NetworkController, val stora
result
}
suspend fun getPreExistingRegistrationData(): PreExistingRegistrationData? {
return storageController.getPreExistingRegistrationData()
}
}

View File

@@ -11,10 +11,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.signal.core.ui.navigation.ResultEventBus
import org.signal.core.util.logging.Log
import kotlin.reflect.KClass
@@ -34,6 +36,14 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
val resultBus = ResultEventBus()
init {
viewModelScope.launch {
repository.getPreExistingRegistrationData()?.let {
_state.value = _state.value.copy(preExistingRegistrationData = it)
}
}
}
fun onEvent(event: RegistrationFlowEvent) {
_state.value = applyEvent(_state.value, event)
}

View File

@@ -5,6 +5,9 @@
package org.signal.registration
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey
import org.signal.core.models.ServiceId.ACI
@@ -12,6 +15,12 @@ import org.signal.core.models.ServiceId.PNI
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.registration.util.ACIParceler
import org.signal.registration.util.AccountEntropyPoolParceler
import org.signal.registration.util.IdentityKeyPairParceler
import org.signal.registration.util.KyberPreKeyRecordParceler
import org.signal.registration.util.PNIParceler
import org.signal.registration.util.SignedPreKeyRecordParceler
interface StorageController {
@@ -63,6 +72,11 @@ interface StorageController {
/**
* Container for all cryptographic key material generated during registration.
*/
@Parcelize
@TypeParceler<IdentityKeyPair, IdentityKeyPairParceler>
@TypeParceler<SignedPreKeyRecord, SignedPreKeyRecordParceler>
@TypeParceler<KyberPreKeyRecord, KyberPreKeyRecordParceler>
@TypeParceler<AccountEntropyPool, AccountEntropyPoolParceler>
data class KeyMaterial(
/** Identity key pair for the Account Identity (ACI). */
val aciIdentityKeyPair: IdentityKeyPair,
@@ -86,7 +100,7 @@ data class KeyMaterial(
val servicePassword: String,
/** Account entropy pool for key derivation. */
val accountEntropyPool: AccountEntropyPool
)
) : Parcelable
data class NewRegistrationData(
val e164: String,
@@ -96,10 +110,14 @@ data class NewRegistrationData(
val aep: AccountEntropyPool
)
@Parcelize
@TypeParceler<AccountEntropyPool, AccountEntropyPoolParceler>
@TypeParceler<ACI, ACIParceler>
@TypeParceler<PNI, PNIParceler>
data class PreExistingRegistrationData(
val e164: String,
val aci: ACI,
val pni: PNI,
val servicePassword: String,
val aep: AccountEntropyPool
)
) : Parcelable

View File

@@ -6,6 +6,7 @@
package org.signal.registration.screens.phonenumber
import org.signal.registration.NetworkController.SessionMetadata
import org.signal.registration.PreExistingRegistrationData
import kotlin.time.Duration
data class PhoneNumberEntryState(
@@ -13,9 +14,11 @@ data class PhoneNumberEntryState(
val countryCode: String = "1",
val nationalNumber: String = "",
val formattedNumber: String = "",
val sessionE164: String? = null,
val sessionMetadata: SessionMetadata? = null,
val showFullScreenSpinner: Boolean = false,
val oneTimeEvent: OneTimeEvent? = null
val oneTimeEvent: OneTimeEvent? = null,
val preExistingRegistrationData: PreExistingRegistrationData? = null
) {
sealed interface OneTimeEvent {
data object NetworkError : OneTimeEvent

View File

@@ -26,7 +26,10 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent
import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent.*
import org.signal.registration.screens.util.navigateTo
import org.signal.registration.screens.verificationcode.VerificationCodeState
import org.signal.registration.screens.verificationcode.VerificationCodeViewModel
class PhoneNumberEntryViewModel(
val repository: RegistrationRepository,
@@ -53,7 +56,7 @@ class PhoneNumberEntryViewModel(
val stateEmitter: (PhoneNumberEntryState) -> Unit = { state ->
_state.value = state
}
applyEvent(_state.value, event, stateEmitter, parentEventEmitter)
applyEvent(state.value, event, stateEmitter, parentEventEmitter)
}
}
@@ -86,7 +89,11 @@ class PhoneNumberEntryViewModel(
@VisibleForTesting
fun applyParentState(state: PhoneNumberEntryState, parentState: RegistrationFlowState): PhoneNumberEntryState {
return state.copy(sessionMetadata = parentState.sessionMetadata)
return state.copy(
sessionE164 = parentState.sessionE164,
sessionMetadata = parentState.sessionMetadata,
preExistingRegistrationData = parentState.preExistingRegistrationData
)
}
private fun applyCountryCodeChanged(state: PhoneNumberEntryState, countryCode: String): PhoneNumberEntryState {
@@ -122,15 +129,6 @@ class PhoneNumberEntryViewModel(
)
}
private fun formatNumber(nationalNumber: String): String {
formatter.clear()
var result = ""
for (digit in nationalNumber) {
result = formatter.inputDigit(digit)
}
return result
}
private suspend fun applyPhoneNumberSubmitted(
inputState: PhoneNumberEntryState,
parentEventEmitter: (RegistrationFlowEvent) -> Unit
@@ -138,7 +136,77 @@ class PhoneNumberEntryViewModel(
val e164 = "+${inputState.countryCode}${inputState.nationalNumber}"
var state = inputState.copy()
// TODO Consider that someone may back into this screen and change the number, requiring us to create a new session.
// If we're re-registering for the same number we used to be registered for, we should try to skip right to registration
if (state.preExistingRegistrationData?.e164 == e164) {
val masterKey = state.preExistingRegistrationData.aep.deriveMasterKey()
val recoveryPassword = masterKey.deriveRegistrationRecoveryPassword()
val registrationLock = masterKey.deriveRegistrationLock()
when (val registerResult = repository.registerAccountWithRecoveryPassword(e164, recoveryPassword, registrationLock, skipDeviceTransfer = true)) {
is NetworkController.RegistrationNetworkResult.Success -> {
val (response, keyMaterial) = registerResult.data
parentEventEmitter(RegistrationFlowEvent.Registered(keyMaterial.accountEntropyPool))
if (response.storageCapable) {
parentEventEmitter.navigateTo(RegistrationRoute.PinEntryForSvrRestore)
} else {
parentEventEmitter.navigateTo(RegistrationRoute.PinCreate)
}
}
is NetworkController.RegistrationNetworkResult.Failure -> {
when (registerResult.error) {
is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> {
Log.w(TAG, "[Register] Got told that our session could not be found when registering with RRP. We should never get into this state. Resetting.")
parentEventEmitter(RegistrationFlowEvent.ResetState)
return state
}
is NetworkController.RegisterAccountError.DeviceTransferPossible -> {
Log.w(TAG, "[Register] Got told a device transfer is possible. We should never get into this state. Resetting.")
parentEventEmitter(RegistrationFlowEvent.ResetState)
return state
}
is NetworkController.RegisterAccountError.RegistrationLock -> {
Log.w(TAG, "[Register] Reglocked.")
parentEventEmitter.navigateTo(
RegistrationRoute.PinEntryForRegistrationLock(
timeRemaining = registerResult.error.data.timeRemaining,
svrCredentials = registerResult.error.data.svr2Credentials
)
)
return state
}
is NetworkController.RegisterAccountError.RateLimited -> {
Log.w(TAG, "[Register] Rate limited.")
return state.copy(oneTimeEvent = OneTimeEvent.RateLimited(registerResult.error.retryAfter))
}
is NetworkController.RegisterAccountError.InvalidRequest -> {
Log.w(TAG, "[Register] Invalid request when registering account with RRP. Ditching pre-existing data and continuing with session creation. Message: ${registerResult.error.message}")
// TODO should we clear it in the parent state as well?
state = state.copy(preExistingRegistrationData = null)
}
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
Log.w(TAG, "[Register] Registration recovery password incorrect. Ditching pre-existing data and continuing with session creation. Message: ${registerResult.error.message}")
// TODO should we clear it in the parent state as well?
state = state.copy(preExistingRegistrationData = null)
}
}
}
is NetworkController.RegistrationNetworkResult.NetworkError -> {
Log.w(TAG, "[Register] Network error.", registerResult.exception)
return state.copy(oneTimeEvent = OneTimeEvent.NetworkError)
}
is NetworkController.RegistrationNetworkResult.ApplicationError -> {
Log.w(TAG, "[Register] Unknown error when registering account.", registerResult.exception)
return state.copy(oneTimeEvent = OneTimeEvent.UnknownError)
}
}
}
// Detect if someone backed into this screen and entered a different number
if (state.sessionE164 != null && state.sessionE164 != e164) {
state = state.copy(sessionMetadata = null)
}
var sessionMetadata: NetworkController.SessionMetadata = state.sessionMetadata ?: when (val response = this@PhoneNumberEntryViewModel.repository.createSession(e164)) {
is NetworkController.RegistrationNetworkResult.Success<NetworkController.SessionMetadata> -> {
@@ -346,6 +414,15 @@ class PhoneNumberEntryViewModel(
return state
}
private fun formatNumber(nationalNumber: String): String {
formatter.clear()
var result = ""
for (digit in nationalNumber) {
result = formatter.inputDigit(digit)
}
return result
}
class Factory(
val repository: RegistrationRepository,
val parentState: StateFlow<RegistrationFlowState>,

View File

@@ -127,7 +127,7 @@ class PinEntryForRegistrationLockViewModel(
}
Log.d(TAG, "[PinEntered] Attempting to register with registration lock token...")
val registerResult = repository.registerAccount(
val registerResult = repository.registerAccountWithSession(
e164 = e164,
sessionId = sessionId,
registrationLock = registrationLockToken,

View File

@@ -130,7 +130,7 @@ class VerificationCodeViewModel(
}
// Attempt to register
val registerResult = repository.registerAccount(e164 = state.e164, sessionId = sessionMetadata.id, skipDeviceTransfer = true)
val registerResult = repository.registerAccountWithSession(e164 = state.e164, sessionId = sessionMetadata.id, skipDeviceTransfer = true)
return when (registerResult) {
is NetworkController.RegistrationNetworkResult.Success -> {
@@ -174,8 +174,9 @@ class VerificationCodeViewModel(
state.copy(oneTimeEvent = OneTimeEvent.RegistrationError)
}
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
Log.w(TAG, "[Register] Registration recovery password incorrect: ${registerResult.error.message}")
state.copy(oneTimeEvent = OneTimeEvent.RegistrationError)
Log.w(TAG, "[Register] Got told the registration recovery password incorrect. We don't use the RRP in this flow, and should never get this error. Resetting. Message: ${registerResult.error.message}")
parentEventEmitter(RegistrationFlowEvent.ResetState)
state
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.core.models.ServiceId
class ACIParceler : Parceler<ServiceId.ACI> {
override fun ServiceId.ACI.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this.toByteArray())
}
override fun create(parcel: Parcel): ServiceId.ACI {
return ServiceId.ACI.parseOrThrow(parcel.createByteArray())
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.core.models.AccountEntropyPool
object AccountEntropyPoolParceler : Parceler<AccountEntropyPool?> {
override fun create(parcel: Parcel): AccountEntropyPool? {
val aep = parcel.readString()
return aep?.let { AccountEntropyPool(it) }
}
override fun AccountEntropyPool?.write(parcel: Parcel, flags: Int) {
parcel.writeString(this?.value)
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.ECPrivateKey
class IdentityKeyPairParceler : Parceler<IdentityKeyPair> {
override fun IdentityKeyPair.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(publicKey.serialize())
parcel.writeByteArray(privateKey.serialize())
}
override fun create(parcel: Parcel): IdentityKeyPair {
return IdentityKeyPair(
IdentityKey(parcel.createByteArray()!!),
ECPrivateKey(parcel.createByteArray()!!)
)
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
class KyberPreKeyRecordParceler : Parceler<KyberPreKeyRecord> {
override fun KyberPreKeyRecord.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this.serialize())
}
override fun create(parcel: Parcel): KyberPreKeyRecord {
return KyberPreKeyRecord(parcel.createByteArray())
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.core.models.MasterKey
object MasterKeyParceler : Parceler<MasterKey?> {
override fun create(parcel: Parcel): MasterKey? {
val bytes = parcel.createByteArray()
return bytes?.let { MasterKey(it) }
}
override fun MasterKey?.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this?.serialize())
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.core.models.ServiceId
class PNIParceler : Parceler<ServiceId.PNI> {
override fun ServiceId.PNI.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this.toByteArray())
}
override fun create(parcel: Parcel): ServiceId.PNI {
return ServiceId.PNI.parseOrThrow(parcel.createByteArray())
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.util
import android.os.Parcel
import kotlinx.parcelize.Parceler
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
class SignedPreKeyRecordParceler : Parceler<SignedPreKeyRecord> {
override fun SignedPreKeyRecord.write(parcel: Parcel, flags: Int) {
parcel.writeByteArray(this.serialize())
}
override fun create(parcel: Parcel): SignedPreKeyRecord {
return SignedPreKeyRecord(parcel.createByteArray())
}
}

View File

@@ -74,7 +74,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(registerResponse to keyMaterial)
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)
@@ -200,7 +200,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RegistrationLock(registrationLockData)
)
@@ -220,7 +220,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RateLimited(retryAfter)
)
@@ -242,7 +242,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.InvalidRequest("Bad request")
)
@@ -261,7 +261,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.DeviceTransferPossible
)
@@ -280,7 +280,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.NetworkError(java.io.IOException("Network error"))
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)
@@ -297,7 +297,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey))
coEvery { mockRepository.registerAccount(any(), any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.ApplicationError(RuntimeException("Unexpected"))
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)

View File

@@ -198,7 +198,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(registerResponse to keyMaterial)
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CodeEntered("123456"))
@@ -266,7 +266,7 @@ class VerificationCodeViewModelTest {
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.SubmitVerificationCodeError.SessionAlreadyVerifiedOrNoCodeRequested(verifiedSession)
)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(registerResponse to keyMaterial)
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CodeEntered("123456"))
@@ -373,7 +373,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.DeviceTransferPossible
)
@@ -395,7 +395,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RateLimited(30.seconds)
)
@@ -422,7 +422,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.InvalidRequest("Bad request")
)
@@ -446,7 +446,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect("Wrong password")
)
@@ -470,7 +470,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.NetworkError(java.io.IOException("Network error"))
val result = viewModel.applyEvent(
@@ -492,7 +492,7 @@ class VerificationCodeViewModelTest {
coEvery { mockRepository.submitVerificationCode(any(), any()) } returns
NetworkController.RegistrationNetworkResult.Success(sessionMetadata)
coEvery { mockRepository.registerAccount(any(), any(), any()) } returns
coEvery { mockRepository.registerAccountWithSession(any(), any(), any()) } returns
NetworkController.RegistrationNetworkResult.ApplicationError(RuntimeException("Unexpected"))
val result = viewModel.applyEvent(