diff --git a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/RealNetworkController.kt b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/RealNetworkController.kt index 3b9048c3b7..cf80256169 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/dependencies/RealNetworkController.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/dependencies/RealNetworkController.kt @@ -289,6 +289,9 @@ class RealNetworkController( fcmToken: String?, skipDeviceTransfer: Boolean ): RegistrationNetworkResult = 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() diff --git a/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt b/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt index b4d933db08..b4c3b4bfbb 100644 --- a/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt +++ b/demo/registration/src/main/java/org/signal/registration/sample/screens/pinsettings/PinSettingsViewModel.kt @@ -178,7 +178,7 @@ class PinSettingsViewModel( ), name = null, pniRegistrationId = RegistrationPreferences.pniRegistrationId, - recoveryPassword = null + recoveryPassword = RegistrationPreferences.masterKey?.deriveRegistrationRecoveryPassword() ) when (val result = networkController.setAccountAttributes(attributes)) { diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt index f31fa8ae97..da9dc16daa 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationActivity.kt @@ -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) diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationFlowState.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationFlowState.kt index b46642bb43..b89a9b4d8e 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationFlowState.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationFlowState.kt @@ -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 -@TypeParceler +@TypeParceler data class RegistrationFlowState( val backStack: List = 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 { - 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 { - 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) - } -} diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 575da41452..0e236ef68f 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -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, 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, 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, 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() + } } diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt index 78a8fc4aaa..b6872a265e 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationViewModel.kt @@ -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) } diff --git a/feature/registration/src/main/java/org/signal/registration/StorageController.kt b/feature/registration/src/main/java/org/signal/registration/StorageController.kt index 6cf34964aa..089edbb0eb 100644 --- a/feature/registration/src/main/java/org/signal/registration/StorageController.kt +++ b/feature/registration/src/main/java/org/signal/registration/StorageController.kt @@ -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 +@TypeParceler +@TypeParceler +@TypeParceler 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 +@TypeParceler +@TypeParceler data class PreExistingRegistrationData( val e164: String, val aci: ACI, val pni: PNI, val servicePassword: String, val aep: AccountEntropyPool -) +) : Parcelable diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt index ead5f1cdb5..219665508d 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt @@ -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 diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt index 3cd77f296d..84ab490b50 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt @@ -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 -> { @@ -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, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt index 27768a3661..67f2423bfe 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt @@ -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, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt index 5aafc020cd..622cf743dc 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt @@ -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 } } } diff --git a/feature/registration/src/main/java/org/signal/registration/util/ACIParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/ACIParceler.kt new file mode 100644 index 0000000000..11168fb62c --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/ACIParceler.kt @@ -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 { + 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()) + } +} \ No newline at end of file diff --git a/feature/registration/src/main/java/org/signal/registration/util/AccountEntropyPoolParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/AccountEntropyPoolParceler.kt new file mode 100644 index 0000000000..d11cca227c --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/AccountEntropyPoolParceler.kt @@ -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 { + 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) + } +} diff --git a/feature/registration/src/main/java/org/signal/registration/util/IdentityKeyPairParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/IdentityKeyPairParceler.kt new file mode 100644 index 0000000000..be63b36b69 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/IdentityKeyPairParceler.kt @@ -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 { + 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()!!) + ) + } +} diff --git a/feature/registration/src/main/java/org/signal/registration/util/KyberPreKeyRecordParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/KyberPreKeyRecordParceler.kt new file mode 100644 index 0000000000..de8e7e69c5 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/KyberPreKeyRecordParceler.kt @@ -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 { + override fun KyberPreKeyRecord.write(parcel: Parcel, flags: Int) { + parcel.writeByteArray(this.serialize()) + } + + override fun create(parcel: Parcel): KyberPreKeyRecord { + return KyberPreKeyRecord(parcel.createByteArray()) + } +} \ No newline at end of file diff --git a/feature/registration/src/main/java/org/signal/registration/util/MasterKeyParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/MasterKeyParceler.kt new file mode 100644 index 0000000000..8574e0d9ae --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/MasterKeyParceler.kt @@ -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 { + 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()) + } +} + diff --git a/feature/registration/src/main/java/org/signal/registration/util/PNIParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/PNIParceler.kt new file mode 100644 index 0000000000..a233574742 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/PNIParceler.kt @@ -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 { + 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()) + } +} \ No newline at end of file diff --git a/feature/registration/src/main/java/org/signal/registration/util/SignedPreKeyRecordParceler.kt b/feature/registration/src/main/java/org/signal/registration/util/SignedPreKeyRecordParceler.kt new file mode 100644 index 0000000000..b5bcbd0060 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/util/SignedPreKeyRecordParceler.kt @@ -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 { + override fun SignedPreKeyRecord.write(parcel: Parcel, flags: Int) { + parcel.writeByteArray(this.serialize()) + } + + override fun create(parcel: Parcel): SignedPreKeyRecord { + return SignedPreKeyRecord(parcel.createByteArray()) + } +} \ No newline at end of file diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt index f0917c52a5..cc0cb0b42a 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt @@ -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) diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt index bea8db8549..a2a62ac21b 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt @@ -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(