Add support for PNI registration ids and PNP change number.

This commit is contained in:
Cody Henthorne
2022-08-03 11:50:16 -04:00
parent 0d3ea22641
commit 83b97d274f
54 changed files with 1273 additions and 188 deletions

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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()
}
)
}
}
}

View File

@@ -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),