mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-14 23:18:43 +00:00
Add basic re-reg support to regV5.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -178,7 +178,7 @@ class PinSettingsViewModel(
|
||||
),
|
||||
name = null,
|
||||
pniRegistrationId = RegistrationPreferences.pniRegistrationId,
|
||||
recoveryPassword = null
|
||||
recoveryPassword = RegistrationPreferences.masterKey?.deriveRegistrationRecoveryPassword()
|
||||
)
|
||||
|
||||
when (val result = networkController.setAccountAttributes(attributes)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user