diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 855da09261..3b21fad208 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -69,7 +69,6 @@ import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob; import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob; import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; -import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob; import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob; @@ -221,7 +220,6 @@ public class ApplicationContext extends Application implements AppForegroundObse .addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary) .addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary) .addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded) - .addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary) .addPostRender(() -> AppDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved()) .addPostRender(() -> AppDependencies.getRecipientCache().warmUp()) .addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index fe802ea103..e273b8308c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob -import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob @@ -818,23 +817,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter sectionHeaderPref(DSLSettingsText.from("PNP")) - clickPref( - title = DSLSettingsText.from("Trigger 'Hello World' event"), - isEnabled = true, - onClick = { - SimpleTask.run(viewLifecycleOwner.lifecycle, { - AppDependencies.jobManager.runSynchronously(PnpInitializeDevicesJob(), 10.seconds.inWholeMilliseconds) - }, { state -> - if (state.isPresent) { - Toast.makeText(context, "Job finished with result: ${state.get()}!", Toast.LENGTH_SHORT).show() - viewModel.refresh() - } else { - Toast.makeText(context, "Job timed out after 10 seconds!", Toast.LENGTH_SHORT).show() - } - }) - } - ) - clickPref( title = DSLSettingsText.from("Reset 'PNP initialized' state"), summary = DSLSettingsText.from("Current initialized state: ${state.pnpInitialized}"), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index fa7b4c3c16..59dbc17b8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -215,7 +215,6 @@ public final class JobManagerFactories { put(PaymentNotificationSendJobV2.KEY, new PaymentNotificationSendJobV2.Factory()); put(PaymentSendJob.KEY, new PaymentSendJob.Factory()); put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); - put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory()); put(PreKeysSyncJob.KEY, new PreKeysSyncJob.Factory()); put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); @@ -393,6 +392,7 @@ public final class JobManagerFactories { put("InactiveGroupCheckMigrationJob", new PassingMigrationJob.Factory()); put("AttachmentMarkUploadedJob", new FailingJob.Factory()); put("BackupMediaSnapshotSyncJob", new FailingJob.Factory()); + put("PnpInitializeDevicesJob", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt deleted file mode 100644 index d0a5355756..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt +++ /dev/null @@ -1,236 +0,0 @@ -package org.thoughtcrime.securesms.jobs - -import androidx.annotation.WorkerThread -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import okio.ByteString.Companion.toByteString -import org.signal.core.util.concurrent.safeBlockingGet -import org.signal.core.util.logging.Log -import org.signal.libsignal.protocol.IdentityKeyPair -import org.signal.libsignal.protocol.SignalProtocolAddress -import org.signal.libsignal.protocol.state.KyberPreKeyRecord -import org.signal.libsignal.protocol.state.SignalProtocolStore -import org.signal.libsignal.protocol.state.SignedPreKeyRecord -import org.signal.libsignal.protocol.util.KeyHelper -import org.signal.libsignal.protocol.util.Medium -import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberViewModel -import org.thoughtcrime.securesms.crypto.PreKeyUtil -import org.thoughtcrime.securesms.dependencies.AppDependencies -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.net.SignalNetwork -import org.whispersystems.signalservice.api.NetworkResult -import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.api.push.SignedPreKeyEntity -import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity -import org.whispersystems.signalservice.internal.push.MismatchedDevices -import org.whispersystems.signalservice.internal.push.OutgoingPushMessage -import org.whispersystems.signalservice.internal.push.SyncMessage -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse -import java.io.IOException -import java.security.SecureRandom - -/** - * To be run when all clients support PNP and we need to initialize all linked devices with appropriate PNP data. - * - * We reuse the change number flow as it already support distributing the necessary data in a way linked devices can understand. - */ -class PnpInitializeDevicesJob private constructor(parameters: Parameters) : BaseJob(parameters) { - - companion object { - const val KEY = "PnpInitializeDevicesJob" - private val TAG = Log.tag(PnpInitializeDevicesJob::class.java) - - @JvmStatic - fun enqueueIfNecessary() { - if (SignalStore.misc.hasPniInitializedDevices || !SignalStore.account.isRegistered || SignalStore.account.aci == null) { - return - } - - AppDependencies.jobManager.add(PnpInitializeDevicesJob()) - } - } - - constructor() : this(Parameters.Builder().addConstraint(NetworkConstraint.KEY).build()) - - override fun serialize(): ByteArray? { - return null - } - - override fun getFactoryKey(): String { - return KEY - } - - override fun onFailure() = Unit - - @Throws(Exception::class) - public override fun onRun() { - if (!SignalStore.account.isRegistered || SignalStore.account.aci == null) { - Log.w(TAG, "Not registered! Skipping, as it wouldn't do anything.") - return - } - - if (!SignalStore.account.hasLinkedDevices) { - Log.i(TAG, "Not multi device, aborting...") - SignalStore.misc.hasPniInitializedDevices = true - return - } - - if (SignalStore.account.isLinkedDevice) { - Log.i(TAG, "Not primary device, aborting...") - SignalStore.misc.hasPniInitializedDevices = true - return - } - - ChangeNumberViewModel.CHANGE_NUMBER_LOCK.lock() - try { - if (SignalStore.misc.hasPniInitializedDevices) { - Log.w(TAG, "We found out that things have been initialized after we got the lock! No need to do anything else.") - return - } - - val e164 = SignalStore.account.requireE164() - - try { - Log.i(TAG, "Initializing PNI for linked devices") - val result: NetworkResult = initializeDevices(e164) - .safeBlockingGet() - - result.getCause()?.let { throw it } - } catch (e: InterruptedException) { - throw IOException("Retry", e) - } catch (t: Throwable) { - Log.w(TAG, "Unable to initialize PNI for linked devices", t) - throw t - } - - SignalStore.misc.hasPniInitializedDevices = true - } finally { - ChangeNumberViewModel.CHANGE_NUMBER_LOCK.unlock() - } - } - - private fun initializeDevices(newE164: String): Single> { - val messageSender = AppDependencies.signalServiceMessageSender - - return Single.fromCallable { - var completed = false - var attempts = 0 - lateinit var distributionResponse: NetworkResult - - while (!completed && attempts < 5) { - val request = createInitializeDevicesRequest( - newE164 = newE164 - ) - - distributionResponse = SignalNetwork.account.distributePniKeys(request) - when (val result = distributionResponse) { - is NetworkResult.Success -> completed = true - is NetworkResult.StatusCodeError -> { - when (result.code) { - 409 -> { - val mismatchedDevices: MismatchedDevices? = result.parseJsonBody() - if (mismatchedDevices != null) { - messageSender.handleChangeNumberMismatchDevices(mismatchedDevices) - } else { - Log.w(TAG, "Unable to parse mismatched devices", result.exception) - } - attempts++ - } - else -> completed = true - } - } - is NetworkResult.NetworkError -> attempts++ - is NetworkResult.ApplicationError -> completed = true - } - } - - distributionResponse - }.subscribeOn(Schedulers.single()) - } - - @WorkerThread - private fun createInitializeDevicesRequest( - newE164: String - ): PniKeyDistributionRequest { - val selfIdentifier: String = SignalStore.account.requireAci().toString() - val aciProtocolStore: SignalProtocolStore = AppDependencies.protocolStore.aci() - val pniProtocolStore: SignalProtocolStore = AppDependencies.protocolStore.pni() - val messageSender = AppDependencies.signalServiceMessageSender - - val pniIdentity: IdentityKeyPair = SignalStore.account.pniIdentityKey - val deviceMessages = mutableListOf() - val devicePniSignedPreKeys = mutableMapOf() - val devicePniLastResortKyberPreKeys = mutableMapOf() - val pniRegistrationIds = mutableMapOf() - val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID - - val devices: List = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier) - - devices - .filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) } - .forEach { deviceId -> - // Signed Prekeys - val signedPreKeyRecord: SignedPreKeyRecord = if (deviceId == primaryDeviceId) { - pniProtocolStore.loadSignedPreKey(SignalStore.account.pniPreKeys.activeSignedPreKeyId) - } else { - PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) - } - devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature) - - // Last-resort kyber prekeys - val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) { - pniProtocolStore.loadKyberPreKey(SignalStore.account.pniPreKeys.lastResortKyberPreKeyId) - } else { - PreKeyUtil.generateLastResortKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) - } - devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature) - - // Registration Ids - var pniRegistrationId = if (deviceId == primaryDeviceId) { - SignalStore.account.pniRegistrationId - } else { - -1 - } - - while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) { - pniRegistrationId = KeyHelper.generateRegistrationId(false) - } - pniRegistrationIds[deviceId] = pniRegistrationId - - // Device Messages - if (deviceId != primaryDeviceId) { - val pniChangeNumber = SyncMessage.PniChangeNumber( - identityKeyPair = pniIdentity.serialize().toByteString(), - signedPreKey = signedPreKeyRecord.serialize().toByteString(), - lastResortKyberPreKey = lastResortKyberPreKeyRecord.serialize().toByteString(), - registrationId = pniRegistrationId, - newE164 = newE164 - ) - - deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber) - } - } - - return PniKeyDistributionRequest( - pniIdentity.publicKey, - deviceMessages, - devicePniSignedPreKeys.mapKeys { it.key.toString() }, - devicePniLastResortKyberPreKeys.mapKeys { it.key.toString() }, - pniRegistrationIds.mapKeys { it.key.toString() }, - true - ) - } - - override fun onShouldRetry(e: Exception): Boolean { - return e is IOException - } - - class Factory : Job.Factory { - override fun create(parameters: Parameters, serializedData: ByteArray?): PnpInitializeDevicesJob { - return PnpInitializeDevicesJob(parameters) - } - } -} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt index 8276ffb4dc..2991d2b344 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt @@ -132,22 +132,6 @@ class AccountApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSock return NetworkResult.fromWebSocketRequest(authWebSocket, request, VerifyAccountResponse::class) } - /** - * Distributes key material to linked devices after an account becomes fully PNP capable. - * - * PUT /v2/accounts/phone_number_identity_key_distribution - * - 200: Success - * - 401: Unauthorized - * - 403: Called from non-primary device - * - 409: Mismatched devices - * - 410: Registration ids do not match - * - 422: Request is malformed - */ - fun distributePniKeys(distributionRequest: PniKeyDistributionRequest): NetworkResult { - val request = WebSocketRequestMessage.put("/v2/accounts/phone_number_identity_key_distribution", distributionRequest) - return NetworkResult.fromWebSocketRequest(authWebSocket, request, VerifyAccountResponse::class) - } - /** * Reserve a username for the account. This replaces an existing reservation if one exists. The username is guaranteed to be available for 5 minutes and can * be confirmed with confirmUsername.