From beee3b7dc38f16db6141234286edc6f314c8dd02 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 9 Sep 2022 20:17:41 -0400 Subject: [PATCH] Add PNP linked device initialization job. Co-authored-by: Greyson Parrelli --- .../securesms/testing/SignalActivityRule.kt | 2 +- .../securesms/AppCapabilities.java | 2 +- .../securesms/ApplicationContext.java | 2 + .../changenumber/ChangeNumberLockActivity.kt | 5 +- .../changenumber/ChangeNumberRepository.kt | 81 ++++++++++--- .../app/changenumber/ChangeNumberViewModel.kt | 4 +- .../securesms/database/RecipientDatabase.kt | 3 + .../database/model/RecipientRecord.kt | 1 + .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/PnpInitializeDevicesJob.kt | 108 ++++++++++++++++++ .../securesms/jobs/RefreshAttributesJob.java | 1 + .../keyvalue/MiscellaneousValues.java | 9 ++ .../securesms/recipients/Recipient.java | 7 ++ .../recipients/RecipientDetails.java | 3 + .../database/RecipientDatabaseTestUtils.kt | 1 + .../api/account/AccountAttributes.java | 10 +- .../api/profiles/SignalServiceProfile.java | 10 +- .../api/account/AccountAttributesTest.java | 48 -------- 18 files changed, 230 insertions(+), 68 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt delete mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index 2022359f77..4caa81939c 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -108,7 +108,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) - SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true)) + SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true)) SignalDatabase.recipients.setProfileSharing(recipientId, true) ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey) others += recipientId diff --git a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java index 18b722c6e9..9a043f092a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java +++ b/app/src/main/java/org/thoughtcrime/securesms/AppCapabilities.java @@ -20,6 +20,6 @@ public final class AppCapabilities { * asking if the user has set a Signal PIN or not. */ public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { - return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadgeReceiveSupport()); + return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadgeReceiveSupport(), FeatureFlags.phoneNumberPrivacy()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 3a1212a01d..ef5ee72880 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FontDownloaderJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; 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.PushNotificationReceiveJob; @@ -206,6 +207,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary) .addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary) .addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded) + .addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary) .execute(); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt index c976940836..5afcd8a36d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt @@ -58,7 +58,10 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() { 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)) + Single + .just(true) + .flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) } + .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) .map { true } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index 3f77a4a973..62df9e4cb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -44,14 +44,44 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic import java.io.IOException import java.security.MessageDigest import java.security.SecureRandom +import java.util.concurrent.locks.ReentrantLock private val TAG: String = Log.tag(ChangeNumberRepository::class.java) +/** + * Provides various change number operations. All operations must run on [Schedulers.single] to support + * the global "I am changing the number" lock exclusivity. + */ class ChangeNumberRepository( private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(), private val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender() ) { + companion object { + /** + * This lock should be held by anyone who is performing a change number operation, so that two different parties cannot change the user's number + * at the same time. + */ + val CHANGE_NUMBER_LOCK = ReentrantLock() + + /** + * Adds Rx operators to chain to acquire and release the [CHANGE_NUMBER_LOCK] on subscribe and on finish. + */ + fun acquireReleaseChangeNumberLock(upstream: Single): Single { + return upstream.doOnSubscribe { + CHANGE_NUMBER_LOCK.lock() + SignalStore.misc().lockChangeNumber() + } + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.single()) + .doFinally { + if (CHANGE_NUMBER_LOCK.isHeldByCurrentThread) { + CHANGE_NUMBER_LOCK.unlock() + } + } + } + } + fun ensureDecryptionsDrained(): Completable { return Completable.create { emitter -> ApplicationDependencies @@ -59,17 +89,23 @@ class ChangeNumberRepository( .addDecryptionDrainedListener { emitter.onComplete() } - }.subscribeOn(Schedulers.io()) + }.subscribeOn(Schedulers.single()) } - fun changeNumber(code: String, newE164: String): Single> { + fun changeNumber(code: String, newE164: String, pniUpdateMode: Boolean = false): Single> { return Single.fromCallable { var completed = false var attempts = 0 lateinit var changeNumberResponse: ServiceResponse while (!completed && attempts < 5) { - val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null) + val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( + code = code, + newE164 = newE164, + registrationLock = null, + pniUpdateMode = pniUpdateMode + ) + SignalStore.misc().setPendingChangeNumberMetadata(metadata) changeNumberResponse = accountManager.changeNumber(request) @@ -84,7 +120,7 @@ class ChangeNumberRepository( } changeNumberResponse - }.subscribeOn(Schedulers.io()) + }.subscribeOn(Schedulers.single()) .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } } @@ -114,7 +150,13 @@ class ChangeNumberRepository( lateinit var changeNumberResponse: ServiceResponse while (!completed && attempts < 5) { - val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock) + val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest( + code = code, + newE164 = newE164, + registrationLock = registrationLock, + pniUpdateMode = false + ) + SignalStore.misc().setPendingChangeNumberMetadata(metadata) changeNumberResponse = accountManager.changeNumber(request) @@ -129,14 +171,14 @@ class ChangeNumberRepository( } VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(changeNumberResponse, kbsData) - }.subscribeOn(Schedulers.io()) + }.subscribeOn(Schedulers.single()) .onErrorReturn { t -> ServiceResponse.forExecutionError(t) } } @Suppress("UsePropertyAccessSyntax") fun whoAmI(): Single { return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() } - .subscribeOn(Schedulers.io()) + .subscribeOn(Schedulers.single()) } @WorkerThread @@ -145,7 +187,7 @@ class ChangeNumberRepository( SignalDatabase.recipients.updateSelfPhone(e164, pni) val newStorageId: ByteArray? = Recipient.self().storageServiceId - if (MessageDigest.isEqual(oldStorageId, newStorageId)) { + if (e164 != SignalStore.account().requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) { Log.w(TAG, "Self storage id was not rotated, attempting to rotate again") SignalDatabase.recipients.rotateStorageId(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() @@ -198,6 +240,9 @@ class ChangeNumberRepository( System.currentTimeMillis(), true ) + + SignalStore.misc().setPniInitializedDevices(true) + ApplicationDependencies.getGroupsV2Authorization().clear() } Recipient.self().live().refresh() @@ -227,7 +272,7 @@ class ChangeNumberRepository( SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate) } - }.subscribeOn(Schedulers.io()) + }.subscribeOn(Schedulers.single()) } @Suppress("UsePropertyAccessSyntax") @@ -235,12 +280,13 @@ class ChangeNumberRepository( private fun createChangeNumberRequest( code: String, newE164: String, - registrationLock: String? + registrationLock: String?, + pniUpdateMode: Boolean ): ChangeNumberRequestData { val selfIdentifier: String = SignalStore.account().requireAci().toString() val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci() - val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() + val pniIdentity: IdentityKeyPair = if (pniUpdateMode) SignalStore.account().pniIdentityKey else IdentityKeyUtil.generateIdentityKeyPair() val deviceMessages = mutableListOf() val devicePniSignedPreKeys = mutableMapOf() val pniRegistrationIds = mutableMapOf() @@ -253,14 +299,23 @@ class ChangeNumberRepository( .forEach { deviceId -> // Signed Prekeys val signedPreKeyRecord = if (deviceId == primaryDeviceId) { - PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey) + if (pniUpdateMode) { + ApplicationDependencies.getProtocolStore().pni().loadSignedPreKey(SignalStore.account().pniPreKeys.activeSignedPreKeyId) + } else { + PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey) + } } 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 + var pniRegistrationId = if (deviceId == primaryDeviceId && pniUpdateMode) { + SignalStore.account().pniRegistrationId + } else { + -1 + } + while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) { pniRegistrationId = KeyHelper.generateRegistrationId(false) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index c73d40a3fe..0a0237fb31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -123,13 +123,13 @@ class ChangeNumberViewModel( override fun verifyCodeWithoutRegistrationLock(code: String): Single { return super.verifyCodeWithoutRegistrationLock(code) - .doOnSubscribe { SignalStore.misc().lockChangeNumber() } + .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) .flatMap(this::attemptToUnlockChangeNumber) } override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single { return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin) - .doOnSubscribe { SignalStore.misc().lockChangeNumber() } + .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock) .flatMap(this::attemptToUnlockChangeNumber) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 92feca11cd..fc2e4cd1bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -1607,6 +1607,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong()) value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong()) value = Bitmask.update(value, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGiftBadges).serialize().toLong()) + value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong()) val values = ContentValues(1).apply { put(CAPABILITIES, value) @@ -3873,6 +3874,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()), storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()), giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()), + pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()), insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)), storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)), mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)), @@ -4192,6 +4194,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : const val CHANGE_NUMBER = 4 const val STORIES = 5 const val GIFT_BADGES = 6 + const val PNP = 7 } enum class VibrateState(val id: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 8346ea86da..d7f5f0057c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -70,6 +70,7 @@ data class RecipientRecord( val changeNumberCapability: Recipient.Capability, val storiesCapability: Recipient.Capability, val giftBadgesCapability: Recipient.Capability, + val pnpCapability: Recipient.Capability, val insightsBannerTier: InsightsBannerTier, val storageId: ByteArray?, val mentionSetting: MentionSetting, 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 94ac440855..b962c303d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -135,6 +135,7 @@ public final class JobManagerFactories { put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt new file mode 100644 index 0000000000..db3ec0e7a2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt @@ -0,0 +1,108 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.concurrent.safeBlockingGet +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberRepository +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs +import org.thoughtcrime.securesms.util.TextSecurePreferences +import java.io.IOException + +/** + * 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) + private const val PLACEHOLDER_CODE = "123456" + + @JvmStatic + fun enqueueIfNecessary() { + if (SignalStore.misc().hasPniInitializedDevices() || !SignalStore.account().isRegistered || SignalStore.account().aci == null || Recipient.self().pnpCapability != Recipient.Capability.SUPPORTED) { + return + } + + ApplicationDependencies.getJobManager().add(PnpInitializeDevicesJob()) + } + } + + constructor() : this(Parameters.Builder().addConstraint(NetworkConstraint.KEY).build()) + + override fun serialize(): Data { + return Data.EMPTY + } + + 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 (!TextSecurePreferences.isMultiDevice(context)) { + Log.i(TAG, "Not multi device, aborting...") + SignalStore.misc().setPniInitializedDevices(true) + return + } + + if (SignalStore.account().isLinkedDevice) { + Log.i(TAG, "Not primary device, aborting...") + SignalStore.misc().setPniInitializedDevices(true) + return + } + + ChangeNumberRepository.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 changeNumberRepository = ChangeNumberRepository() + val e164 = SignalStore.account().requireE164() + + try { + Log.i(TAG, "Calling change number with our current number to distribute PNI messages") + changeNumberRepository + .changeNumber(code = PLACEHOLDER_CODE, newE164 = e164, pniUpdateMode = true) + .map(::VerifyAccountResponseWithoutKbs) + .safeBlockingGet() + .resultOrThrow + } 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().setPniInitializedDevices(true) + } finally { + ChangeNumberRepository.CHANGE_NUMBER_LOCK.unlock() + } + } + + override fun onShouldRetry(e: Exception): Boolean { + return e is IOException + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): PnpInitializeDevicesJob { + return PnpInitializeDevicesJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 2d0edcee1d..58e8a7a7ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -117,6 +117,7 @@ public class RefreshAttributesJob extends BaseJob { "\n Change Number? " + capabilities.isChangeNumber() + "\n Stories? " + capabilities.isStories() + "\n Gift Badges? " + capabilities.isGiftBadges() + + "\n PNP? " + capabilities.isPnp() + "\n UUID? " + capabilities.isUuid()); SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index e832554ced..8b35d39b5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -26,6 +26,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String CDS_TOKEN = "misc.cds_token"; private static final String LAST_FCM_FOREGROUND_TIME = "misc.last_fcm_foreground_time"; private static final String LAST_FOREGROUND_TIME = "misc.last_foreground_time"; + private static final String PNI_INITIALIZED_DEVICES = "misc.pni_initialized_devices"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -184,4 +185,12 @@ public final class MiscellaneousValues extends SignalStoreValues { public void setLastForegroundTime(long time) { putLong(LAST_FOREGROUND_TIME, time); } + + public boolean hasPniInitializedDevices() { + return getBoolean(PNI_INITIALIZED_DEVICES, false); + } + + public void setPniInitializedDevices(boolean value) { + putBoolean(PNI_INITIALIZED_DEVICES, value); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 3c7c7601cb..7dcec41468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -124,6 +124,7 @@ public class Recipient { private final Capability changeNumberCapability; private final Capability storiesCapability; private final Capability giftBadgesCapability; + private final Capability pnpCapability; private final InsightsBannerTier insightsBannerTier; private final byte[] storageId; private final MentionSetting mentionSetting; @@ -426,6 +427,7 @@ public class Recipient { this.changeNumberCapability = Capability.UNKNOWN; this.storiesCapability = Capability.UNKNOWN; this.giftBadgesCapability = Capability.UNKNOWN; + this.pnpCapability = Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; @@ -485,6 +487,7 @@ public class Recipient { this.changeNumberCapability = details.changeNumberCapability; this.storiesCapability = details.storiesCapability; this.giftBadgesCapability = details.giftBadgesCapability; + this.pnpCapability = details.pnpCapability; this.storageId = details.storageId; this.mentionSetting = details.mentionSetting; this.wallpaper = details.wallpaper; @@ -1043,6 +1046,10 @@ public class Recipient { return giftBadgesCapability; } + public @NonNull Capability getPnpCapability() { + return pnpCapability; + } + /** * True if this recipient supports the message retry system, or false if we should use the legacy session reset system. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index 25bf12ed6c..98be1ce31a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -75,6 +75,7 @@ public class RecipientDetails { final Recipient.Capability changeNumberCapability; final Recipient.Capability storiesCapability; final Recipient.Capability giftBadgesCapability; + final Recipient.Capability pnpCapability; final InsightsBannerTier insightsBannerTier; final byte[] storageId; final MentionSetting mentionSetting; @@ -139,6 +140,7 @@ public class RecipientDetails { this.changeNumberCapability = record.getChangeNumberCapability(); this.storiesCapability = record.getStoriesCapability(); this.giftBadgesCapability = record.getGiftBadgesCapability(); + this.pnpCapability = record.getPnpCapability(); this.insightsBannerTier = record.getInsightsBannerTier(); this.storageId = record.getStorageId(); this.mentionSetting = record.getMentionSetting(); @@ -199,6 +201,7 @@ public class RecipientDetails { this.changeNumberCapability = Recipient.Capability.UNKNOWN; this.storiesCapability = Recipient.Capability.UNKNOWN; this.giftBadgesCapability = Recipient.Capability.UNKNOWN; + this.pnpCapability = Recipient.Capability.UNKNOWN; this.storageId = null; this.mentionSetting = MentionSetting.ALWAYS_NOTIFY; this.wallpaper = null; diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 88b56bbf4d..d8895328cb 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -133,6 +133,7 @@ object RecipientDatabaseTestUtils { Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.CHANGE_NUMBER, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.STORIES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.GIFT_BADGES, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), + Recipient.Capability.deserialize(Bitmask.read(capabilities, RecipientDatabase.Capabilities.PNP, RecipientDatabase.Capabilities.BIT_LENGTH).toInt()), insightBannerTier, storageId, mentionSetting, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java index 18f8c79a95..9ec144d866 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java @@ -159,10 +159,13 @@ public class AccountAttributes { @JsonProperty private boolean giftBadges; + @JsonProperty + private boolean pnp; + @JsonCreator public Capabilities() {} - public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories, boolean giftBadges) { + public Capabilities(boolean uuid, boolean gv2, boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories, boolean giftBadges, boolean pnp) { this.uuid = uuid; this.gv2 = gv2; this.storage = storage; @@ -172,6 +175,7 @@ public class AccountAttributes { this.changeNumber = changeNumber; this.stories = stories; this.giftBadges = giftBadges; + this.pnp = pnp; } public boolean isUuid() { @@ -209,5 +213,9 @@ public class AccountAttributes { public boolean isGiftBadges() { return giftBadges; } + + public boolean isPnp() { + return pnp; + } } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index c4a4b9d8a8..481326aed2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -203,10 +203,13 @@ public class SignalServiceProfile { @JsonProperty private boolean giftBadges; + @JsonProperty + private boolean pnp; + @JsonCreator public Capabilities() {} - public Capabilities(boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories, boolean giftBadges) { + public Capabilities(boolean storage, boolean gv1Migration, boolean senderKey, boolean announcementGroup, boolean changeNumber, boolean stories, boolean giftBadges, boolean pnp) { this.storage = storage; this.gv1Migration = gv1Migration; this.senderKey = senderKey; @@ -214,6 +217,7 @@ public class SignalServiceProfile { this.changeNumber = changeNumber; this.stories = stories; this.giftBadges = giftBadges; + this.pnp = pnp; } public boolean isStorage() { @@ -243,6 +247,10 @@ public class SignalServiceProfile { public boolean isGiftBadges() { return giftBadges; } + + public boolean isPnp() { + return pnp; + } } public ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse() { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java deleted file mode 100644 index 0763dfb6d7..0000000000 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.whispersystems.signalservice.api.account; - -import org.junit.Test; -import org.whispersystems.signalservice.internal.util.JsonUtil; - -import static org.junit.Assert.assertEquals; - -public final class AccountAttributesTest { - - @Test - public void can_write_account_attributes() { - String json = JsonUtil.toJson(new AccountAttributes("skey", - 123, - true, - "1234", - "reglock1234", - new byte[10], - false, - new AccountAttributes.Capabilities(true, true, true, true, true, true, true, true, true), - false, - null, - 321)); - assertEquals("{\"signalingKey\":\"skey\"," + - "\"registrationId\":123," + - "\"voice\":true," + - "\"video\":true," + - "\"fetchesMessages\":true," + - "\"pin\":\"1234\"," + - "\"registrationLock\":\"reglock1234\"," + - "\"unidentifiedAccessKey\":\"AAAAAAAAAAAAAA==\"," + - "\"unrestrictedUnidentifiedAccess\":false," + - "\"discoverableByPhoneNumber\":false," + - "\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"stories\":true,\"giftBadges\":true,\"gv2-3\":true,\"gv1-migration\":true}," + - "\"name\":null,\"pniRegistrationId\":321}", json); - } - - @Test - public void gv2_true() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, true, false, false, false, false, false, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"giftBadges\":false,\"gv2-3\":true,\"gv1-migration\":false}", json); - } - - @Test - public void gv2_false() { - String json = JsonUtil.toJson(new AccountAttributes.Capabilities(false, false, false, false, false, false, false, false, false)); - assertEquals("{\"uuid\":false,\"storage\":false,\"senderKey\":false,\"announcementGroup\":false,\"changeNumber\":false,\"stories\":false,\"giftBadges\":false,\"gv2-3\":false,\"gv1-migration\":false}", json); - } -} \ No newline at end of file