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?, fcmToken: String?,
skipDeviceTransfer: Boolean skipDeviceTransfer: Boolean
): RegistrationNetworkResult<RegisterAccountResponse, RegisterAccountError> = withContext(Dispatchers.IO) { ): 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 { try {
val serviceAttributes = attributes.toServiceAccountAttributes() val serviceAttributes = attributes.toServiceAccountAttributes()
val serviceAciPreKeys = aciPreKeys.toServicePreKeyCollection() val serviceAciPreKeys = aciPreKeys.toServicePreKeyCollection()

View File

@@ -178,7 +178,7 @@ class PinSettingsViewModel(
), ),
name = null, name = null,
pniRegistrationId = RegistrationPreferences.pniRegistrationId, pniRegistrationId = RegistrationPreferences.pniRegistrationId,
recoveryPassword = null recoveryPassword = RegistrationPreferences.masterKey?.deriveRegistrationRecoveryPassword()
) )
when (val result = networkController.setAccountAttributes(attributes)) { 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) @OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -5,44 +5,24 @@
package org.signal.registration package org.signal.registration
import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler import kotlinx.parcelize.TypeParceler
import org.signal.core.models.AccountEntropyPool import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey import org.signal.core.models.MasterKey
import org.signal.registration.util.AccountEntropyPoolParceler
import org.signal.registration.util.MasterKeyParceler
@Parcelize @Parcelize
@TypeParceler<MasterKey?, MasterKeyParceler> @TypeParceler<MasterKey?, MasterKeyParceler>
@TypeParceler<AccountEntropyPool?, AepParceler> @TypeParceler<AccountEntropyPool?, AccountEntropyPoolParceler>
data class RegistrationFlowState( data class RegistrationFlowState(
val backStack: List<RegistrationRoute> = listOf(RegistrationRoute.Welcome), val backStack: List<RegistrationRoute> = listOf(RegistrationRoute.Welcome),
val sessionMetadata: NetworkController.SessionMetadata? = null, val sessionMetadata: NetworkController.SessionMetadata? = null,
val sessionE164: String? = null, val sessionE164: String? = null,
val accountEntropyPool: AccountEntropyPool? = null, val accountEntropyPool: AccountEntropyPool? = null,
val temporaryMasterKey: MasterKey? = null, val temporaryMasterKey: MasterKey? = null,
val registrationLockProof: String? = null val registrationLockProof: String? = null,
val preExistingRegistrationData: PreExistingRegistrationData? = null
) : Parcelable ) : 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. * 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 * @param skipDeviceTransfer Whether to skip device transfer flow
* @return The registration result containing account information or an error * @return The registration result containing account information or an error
*/ */
suspend fun registerAccount( suspend fun registerAccountWithSession(
e164: String, e164: String,
sessionId: String, sessionId: String,
registrationLock: String? = null, registrationLock: String? = null,
skipDeviceTransfer: Boolean = true skipDeviceTransfer: Boolean = true
): RegistrationNetworkResult<Pair<RegisterAccountResponse, KeyMaterial>, RegisterAccountError> = withContext(Dispatchers.IO) { ): 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 keyMaterial = storageController.generateAndStoreKeyMaterial()
val fcmToken = networkController.getFcmToken() val fcmToken = networkController.getFcmToken()
@@ -150,7 +203,7 @@ class RegistrationRepository(val networkController: NetworkController, val stora
), ),
name = null, name = null,
pniRegistrationId = keyMaterial.pniRegistrationId, pniRegistrationId = keyMaterial.pniRegistrationId,
recoveryPassword = null recoveryPassword = keyMaterial.accountEntropyPool.deriveMasterKey().deriveRegistrationRecoveryPassword()
) )
val aciPreKeys = PreKeyCollection( val aciPreKeys = PreKeyCollection(
@@ -169,7 +222,7 @@ class RegistrationRepository(val networkController: NetworkController, val stora
e164 = e164, e164 = e164,
password = keyMaterial.servicePassword, password = keyMaterial.servicePassword,
sessionId = sessionId, sessionId = sessionId,
recoveryPassword = null, recoveryPassword = recoveryPassword,
attributes = accountAttributes, attributes = accountAttributes,
aciPreKeys = aciPreKeys, aciPreKeys = aciPreKeys,
pniPreKeys = pniPreKeys, pniPreKeys = pniPreKeys,
@@ -205,4 +258,8 @@ class RegistrationRepository(val networkController: NetworkController, val stora
result 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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.signal.core.ui.navigation.ResultEventBus import org.signal.core.ui.navigation.ResultEventBus
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -34,6 +36,14 @@ class RegistrationViewModel(private val repository: RegistrationRepository, save
val resultBus = ResultEventBus() val resultBus = ResultEventBus()
init {
viewModelScope.launch {
repository.getPreExistingRegistrationData()?.let {
_state.value = _state.value.copy(preExistingRegistrationData = it)
}
}
}
fun onEvent(event: RegistrationFlowEvent) { fun onEvent(event: RegistrationFlowEvent) {
_state.value = applyEvent(_state.value, event) _state.value = applyEvent(_state.value, event)
} }

View File

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

View File

@@ -6,6 +6,7 @@
package org.signal.registration.screens.phonenumber package org.signal.registration.screens.phonenumber
import org.signal.registration.NetworkController.SessionMetadata import org.signal.registration.NetworkController.SessionMetadata
import org.signal.registration.PreExistingRegistrationData
import kotlin.time.Duration import kotlin.time.Duration
data class PhoneNumberEntryState( data class PhoneNumberEntryState(
@@ -13,9 +14,11 @@ data class PhoneNumberEntryState(
val countryCode: String = "1", val countryCode: String = "1",
val nationalNumber: String = "", val nationalNumber: String = "",
val formattedNumber: String = "", val formattedNumber: String = "",
val sessionE164: String? = null,
val sessionMetadata: SessionMetadata? = null, val sessionMetadata: SessionMetadata? = null,
val showFullScreenSpinner: Boolean = false, val showFullScreenSpinner: Boolean = false,
val oneTimeEvent: OneTimeEvent? = null val oneTimeEvent: OneTimeEvent? = null,
val preExistingRegistrationData: PreExistingRegistrationData? = null
) { ) {
sealed interface OneTimeEvent { sealed interface OneTimeEvent {
data object NetworkError : 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.RegistrationRepository
import org.signal.registration.RegistrationRoute 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.phonenumber.PhoneNumberEntryState.OneTimeEvent.*
import org.signal.registration.screens.util.navigateTo import org.signal.registration.screens.util.navigateTo
import org.signal.registration.screens.verificationcode.VerificationCodeState
import org.signal.registration.screens.verificationcode.VerificationCodeViewModel
class PhoneNumberEntryViewModel( class PhoneNumberEntryViewModel(
val repository: RegistrationRepository, val repository: RegistrationRepository,
@@ -53,7 +56,7 @@ class PhoneNumberEntryViewModel(
val stateEmitter: (PhoneNumberEntryState) -> Unit = { state -> val stateEmitter: (PhoneNumberEntryState) -> Unit = { state ->
_state.value = state _state.value = state
} }
applyEvent(_state.value, event, stateEmitter, parentEventEmitter) applyEvent(state.value, event, stateEmitter, parentEventEmitter)
} }
} }
@@ -86,7 +89,11 @@ class PhoneNumberEntryViewModel(
@VisibleForTesting @VisibleForTesting
fun applyParentState(state: PhoneNumberEntryState, parentState: RegistrationFlowState): PhoneNumberEntryState { 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 { 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( private suspend fun applyPhoneNumberSubmitted(
inputState: PhoneNumberEntryState, inputState: PhoneNumberEntryState,
parentEventEmitter: (RegistrationFlowEvent) -> Unit parentEventEmitter: (RegistrationFlowEvent) -> Unit
@@ -138,7 +136,77 @@ class PhoneNumberEntryViewModel(
val e164 = "+${inputState.countryCode}${inputState.nationalNumber}" val e164 = "+${inputState.countryCode}${inputState.nationalNumber}"
var state = inputState.copy() 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)) { var sessionMetadata: NetworkController.SessionMetadata = state.sessionMetadata ?: when (val response = this@PhoneNumberEntryViewModel.repository.createSession(e164)) {
is NetworkController.RegistrationNetworkResult.Success<NetworkController.SessionMetadata> -> { is NetworkController.RegistrationNetworkResult.Success<NetworkController.SessionMetadata> -> {
@@ -346,6 +414,15 @@ class PhoneNumberEntryViewModel(
return state return state
} }
private fun formatNumber(nationalNumber: String): String {
formatter.clear()
var result = ""
for (digit in nationalNumber) {
result = formatter.inputDigit(digit)
}
return result
}
class Factory( class Factory(
val repository: RegistrationRepository, val repository: RegistrationRepository,
val parentState: StateFlow<RegistrationFlowState>, val parentState: StateFlow<RegistrationFlowState>,

View File

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

View File

@@ -130,7 +130,7 @@ class VerificationCodeViewModel(
} }
// Attempt to register // 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) { return when (registerResult) {
is NetworkController.RegistrationNetworkResult.Success -> { is NetworkController.RegistrationNetworkResult.Success -> {
@@ -174,8 +174,9 @@ class VerificationCodeViewModel(
state.copy(oneTimeEvent = OneTimeEvent.RegistrationError) state.copy(oneTimeEvent = OneTimeEvent.RegistrationError)
} }
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> { is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
Log.w(TAG, "[Register] Registration recovery password incorrect: ${registerResult.error.message}") 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}")
state.copy(oneTimeEvent = OneTimeEvent.RegistrationError) 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 coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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) NetworkController.RegistrationNetworkResult.Success(registerResponse to keyMaterial)
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter) viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)
@@ -200,7 +200,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RegistrationLock(registrationLockData) NetworkController.RegisterAccountError.RegistrationLock(registrationLockData)
) )
@@ -220,7 +220,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.RateLimited(retryAfter) NetworkController.RegisterAccountError.RateLimited(retryAfter)
) )
@@ -242,7 +242,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.InvalidRequest("Bad request") NetworkController.RegisterAccountError.InvalidRequest("Bad request")
) )
@@ -261,7 +261,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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.RegistrationNetworkResult.Failure(
NetworkController.RegisterAccountError.DeviceTransferPossible NetworkController.RegisterAccountError.DeviceTransferPossible
) )
@@ -280,7 +280,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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")) NetworkController.RegistrationNetworkResult.NetworkError(java.io.IOException("Network error"))
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter) viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)
@@ -297,7 +297,7 @@ class PinEntryForRegistrationLockViewModelTest {
coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), any(), forRegistrationLock = true) } returns
NetworkController.RegistrationNetworkResult.Success(NetworkController.MasterKeyResponse(masterKey)) 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")) NetworkController.RegistrationNetworkResult.ApplicationError(RuntimeException("Unexpected"))
viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter) viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), stateEmitter, parentEventEmitter)

View File

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