mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 10:20:25 +01:00
Add support for PNI registration ids and PNP change number.
This commit is contained in:
@@ -38,7 +38,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
changeNumberRepository = ChangeNumberRepository(applicationContext)
|
||||
changeNumberRepository = ChangeNumberRepository()
|
||||
checkWhoAmI()
|
||||
}
|
||||
|
||||
@@ -50,25 +50,25 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
override fun onBackPressed() = Unit
|
||||
|
||||
private fun checkWhoAmI() {
|
||||
disposables.add(
|
||||
changeNumberRepository.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
.map { true }
|
||||
}
|
||||
disposables += changeNumberRepository
|
||||
.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
.map { true }
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
}
|
||||
|
||||
private fun onChangeStatusConfirmed() {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.state.SignalProtocolStore
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -17,22 +27,45 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
class ChangeNumberRepository(private val context: Context) {
|
||||
class ChangeNumberRepository(private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()) {
|
||||
|
||||
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return Completable.create { emitter ->
|
||||
ApplicationDependencies
|
||||
.getIncomingMessageObserver()
|
||||
.addDecryptionDrainedListener {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
return Single.fromCallable {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null)
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
accountManager.changeNumber(request)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
fun changeNumber(
|
||||
@@ -45,8 +78,11 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock)
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(request)
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
@@ -56,6 +92,7 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@@ -86,6 +123,49 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
SignalStore.account().setE164(e164)
|
||||
SignalStore.account().setPni(pni)
|
||||
|
||||
ApplicationDependencies.getGroupsV2Authorization().clear()
|
||||
|
||||
val metadata: PendingChangeNumberMetadata? = SignalStore.misc().pendingChangeNumberMetadata
|
||||
if (metadata == null) {
|
||||
Log.w(TAG, "No change number metadata, this shouldn't happen")
|
||||
throw AssertionError("No change number metadata")
|
||||
}
|
||||
|
||||
val originalPni = ServiceId.fromByteString(metadata.previousPni)
|
||||
|
||||
if (originalPni == pni) {
|
||||
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
|
||||
} else {
|
||||
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
|
||||
val pniRegistrationId = metadata.pniRegistrationId
|
||||
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
|
||||
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
SignalStore.account().pniRegistrationId = pniRegistrationId
|
||||
SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
|
||||
|
||||
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
|
||||
val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(pniProtocolStore, pniMetadataStore)
|
||||
|
||||
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
|
||||
accountManager.setPreKeys(ServiceIdType.PNI, pniProtocolStore.identityKeyPair.publicKey, signedPreKey, oneTimePreKeys)
|
||||
pniMetadataStore.isSignedPreKeyRegistered = true
|
||||
|
||||
pniProtocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
Recipient.self().id,
|
||||
pniProtocolStore.identityKeyPair.publicKey,
|
||||
IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh()
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
@@ -112,4 +192,66 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@WorkerThread
|
||||
private fun createChangeNumberRequest(
|
||||
code: String,
|
||||
newE164: String,
|
||||
registrationLock: String?
|
||||
): ChangeNumberRequestData {
|
||||
val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
|
||||
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore: PreKeyMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
val devices: List<DeviceInfo> = accountManager.getDevices()
|
||||
|
||||
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
val deviceMessages = mutableListOf<OutgoingPushMessage>()
|
||||
val devicePniSignedPreKeys = mutableMapOf<String, SignedPreKeyEntity>()
|
||||
val pniRegistrationIds = mutableMapOf<String, Int>()
|
||||
val primaryDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID.toString()
|
||||
|
||||
for (device in devices) {
|
||||
val deviceId = device.id.toString()
|
||||
|
||||
// Signed Prekeys
|
||||
val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
PreKeyUtil.generateAndStoreSignedPreKey(pniProtocolStore, pniMetadataStore, pniIdentity.privateKey, false)
|
||||
} else {
|
||||
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
}
|
||||
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
|
||||
// Registration Ids
|
||||
var pniRegistrationId = -1
|
||||
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
}
|
||||
pniRegistrationIds[deviceId] = pniRegistrationId
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
|
||||
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
|
||||
.setRegistrationId(pniRegistrationId)
|
||||
.build()
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(device.id, pniChangeNumber)
|
||||
}
|
||||
}
|
||||
|
||||
val request = ChangePhoneNumberRequest(newE164, code, registrationLock, pniIdentity.publicKey, deviceMessages, devicePniSignedPreKeys, pniRegistrationIds)
|
||||
val metadata = PendingChangeNumberMetadata.newBuilder()
|
||||
.setPreviousPni(SignalStore.account().pni!!.toByteString())
|
||||
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
|
||||
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
|
||||
.build()
|
||||
|
||||
return ChangeNumberRequestData(request, metadata)
|
||||
}
|
||||
|
||||
data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
|
||||
}
|
||||
|
||||
@@ -48,29 +48,29 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
lifecycleDisposable.add(
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
lifecycleDisposable += viewModel
|
||||
.ensureDecryptionsDrained()
|
||||
.andThen(viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -107,6 +108,10 @@ class ChangeNumberViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return changeNumberRepository.ensureDecryptionsDrained()
|
||||
}
|
||||
|
||||
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
|
||||
return super.verifyCodeWithoutRegistrationLock(code)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
@@ -122,6 +127,7 @@ class ChangeNumberViewModel(
|
||||
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
|
||||
return if (processor.hasResult() || processor.isServerSentError()) {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
Single.just(processor)
|
||||
} else {
|
||||
changeNumberRepository.whoAmI()
|
||||
@@ -129,6 +135,7 @@ class ChangeNumberViewModel(
|
||||
if (Objects.equals(whoAmI.number, localNumber)) {
|
||||
Log.i(TAG, "Local and remote numbers match, we can unlock.")
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
}
|
||||
processor
|
||||
}
|
||||
@@ -172,7 +179,7 @@ class ChangeNumberViewModel(
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
changeNumberRepository = ChangeNumberRepository(context),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = handle,
|
||||
password = password,
|
||||
verifyAccountRepository = VerifyAccountRepository(context),
|
||||
|
||||
Reference in New Issue
Block a user