From dec1902dc74df3d97b729e6221df7454c2b3a94a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 2 Feb 2022 18:35:16 -0500 Subject: [PATCH] Add provisioning support for PNP. --- .../database/RecipientDatabaseTest.kt | 6 + .../database/RecipientDatabaseTest_merges.kt | 12 +- .../securesms/DeviceActivity.java | 10 +- .../securesms/crypto/ProfileKeyUtil.java | 12 -- .../securesms/database/RecipientDatabase.kt | 2 +- .../securesms/database/SessionDatabase.kt | 1 + .../ApplicationDependencyProvider.java | 5 + .../securesms/jobs/JobManagerFactories.java | 1 + .../jobs/MultiDevicePniIdentityUpdateJob.java | 103 ++++++++++++++++++ .../securesms/jobs/PushSendJob.java | 2 +- .../securesms/jobs/RetrieveProfileJob.java | 2 +- .../messages/MessageContentProcessor.java | 5 + .../securesms/push/AccountManagerFactory.java | 7 +- .../registration/RegistrationRepository.java | 2 +- .../secondary/SecondaryProvisioningCipher.kt | 4 +- .../SecondaryProvisioningCipherTest.kt | 8 +- .../api/SignalServiceAccountManager.java | 52 ++++----- .../messages/multidevice/RequestMessage.java | 4 + .../multidevice/SignalServiceSyncMessage.java | 48 +++++++- .../api/util/CredentialsProvider.java | 2 + .../util/StaticCredentialsProvider.java | 12 +- .../service/src/main/proto/Provisioning.proto | 22 ++-- .../src/main/proto/SignalService.proto | 8 ++ 23 files changed, 261 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDevicePniIdentityUpdateJob.java diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt index f1c92d08c9..06c61fbf6e 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt @@ -30,10 +30,16 @@ class RecipientDatabaseTest { private lateinit var recipientDatabase: RecipientDatabase + private val localAci = ACI.from(UUID.randomUUID()); + private val localPni = PNI.from(UUID.randomUUID()); + @Before fun setup() { recipientDatabase = SignalDatabase.recipients ensureDbEmpty() + + SignalStore.account().setAci(localAci) + SignalStore.account().setPni(localPni) } // ============================================================== diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt index e59181ea96..7a0b13386c 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.IncomingMediaMessage import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.recipients.Recipient @@ -33,6 +34,7 @@ import org.whispersystems.libsignal.SignalProtocolAddress import org.whispersystems.libsignal.state.SessionRecord import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.signalservice.api.push.ACI +import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.util.UuidUtil import java.util.UUID @@ -51,6 +53,9 @@ class RecipientDatabaseTest_merges { private lateinit var reactionDatabase: ReactionDatabase private lateinit var notificationProfileDatabase: NotificationProfileDatabase + private val localAci = ACI.from(UUID.randomUUID()); + private val localPni = PNI.from(UUID.randomUUID()); + @Before fun setup() { recipientDatabase = SignalDatabase.recipients @@ -65,6 +70,9 @@ class RecipientDatabaseTest_merges { reactionDatabase = SignalDatabase.reactions notificationProfileDatabase = SignalDatabase.notificationProfiles + SignalStore.account().setAci(localAci) + SignalStore.account().setPni(localPni) + ensureDbEmpty() } @@ -99,7 +107,7 @@ class RecipientDatabaseTest_merges { identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false) identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false) - sessionDatabase.store(SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord()) + sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord()) reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1)) reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1)) @@ -175,7 +183,7 @@ class RecipientDatabaseTest_merges { assertNull(identityDatabase.getIdentityStoreRecord(E164_A)) // Session validation - assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1))) + assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1))) // Reaction validation val reactionsSms: List = reactionDatabase.getReactions(MessageId(smsId1, false)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index 8ec210d066..b2d8b18ff8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; +import org.signal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -187,12 +188,13 @@ public class DeviceActivity extends PassphraseRequiredActivity return BAD_CODE; } - ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - IdentityKeyPair identityKeyPair = SignalStore.account().getAciIdentityKey(); - Optional profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext())); + ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + IdentityKeyPair aciIdentityKeyPair = SignalStore.account().getAciIdentityKey(); + IdentityKeyPair pniIdentityKeyPair = SignalStore.account().getPniIdentityKey(); + ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey(); TextSecurePreferences.setMultiDevice(DeviceActivity.this, true); - accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode); + accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, verificationCode); return SUCCESS; } catch (NotFoundException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java index 688d1a481f..6d1d3b04e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ProfileKeyUtil.java @@ -22,18 +22,6 @@ public final class ProfileKeyUtil { private ProfileKeyUtil() { } - /** @deprecated Use strongly typed {@link org.signal.zkgroup.profiles.ProfileKey} - * from {@link #getSelfProfileKey()} - * or {@code getSelfProfileKey().serialize()} if you need the bytes. */ - @Deprecated - public static @NonNull byte[] getProfileKey(@NonNull Context context) { - byte[] profileKey = Recipient.self().getProfileKey(); - if (profileKey == null) { - throw new AssertionError(); - } - return profileKey; - } - public static synchronized @NonNull ProfileKey getSelfProfileKey() { try { return new ProfileKey(Recipient.self().getProfileKey()); 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 8e33c16a33..b14245547b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -2604,7 +2604,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } // Sessions - val localAci: ACI = Recipient.self().aci.get() + val localAci: ACI = SignalStore.account().aci!! val sessionDatabase = sessions val hasE164Session = sessionDatabase.getAllFor(localAci, e164Record.e164).isNotEmpty() val hasAciSession = sessionDatabase.getAllFor(localAci, aciRecord.aci.toString()).isNotEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.kt index 6fda962227..94a419b3e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SessionDatabase.kt @@ -32,6 +32,7 @@ class SessionDatabase(context: Context, databaseHelper: SignalDatabase) : Databa $DEVICE INTEGER NOT NULL, $RECORD BLOB NOT NULL, UNIQUE($ACCOUNT_ID, $ADDRESS, $DEVICE) + ) """ } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 1e9a6559ec..3a3f81c26b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -370,6 +370,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return SignalStore.account().getAci(); } + @Override + public PNI getPni() { + return SignalStore.account().getPni(); + } + @Override public String getE164() { return SignalStore.account().getE164(); 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 2338ba1081..f4a4644e9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -112,6 +112,7 @@ public final class JobManagerFactories { put(MultiDeviceKeysUpdateJob.KEY, new MultiDeviceKeysUpdateJob.Factory()); put(MultiDeviceMessageRequestResponseJob.KEY, new MultiDeviceMessageRequestResponseJob.Factory()); put(MultiDeviceOutgoingPaymentSyncJob.KEY, new MultiDeviceOutgoingPaymentSyncJob.Factory()); + put(MultiDevicePniIdentityUpdateJob.KEY, new MultiDevicePniIdentityUpdateJob.Factory()); put(MultiDeviceProfileContentUpdateJob.KEY, new MultiDeviceProfileContentUpdateJob.Factory()); put(MultiDeviceProfileKeyUpdateJob.KEY, new MultiDeviceProfileKeyUpdateJob.Factory()); put(MultiDeviceReadUpdateJob.KEY, new MultiDeviceReadUpdateJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDevicePniIdentityUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDevicePniIdentityUpdateJob.java new file mode 100644 index 0000000000..7c8667d6d8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDevicePniIdentityUpdateJob.java @@ -0,0 +1,103 @@ +package org.thoughtcrime.securesms.jobs; + + +import androidx.annotation.NonNull; + +import com.google.protobuf.ByteString; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; +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.net.NotPushRegisteredException; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.PniIdentity; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * As part of the PNI migration, linked devices will need to be told what their PNI identity key is. This job is sent either in response to a request from + * a linked device or as part of a migration when we start using PNIs. + */ +public class MultiDevicePniIdentityUpdateJob extends BaseJob { + + private static final String TAG = Log.tag(MultiDevicePniIdentityUpdateJob.class); + + public static final String KEY = "MultiDevicePniIdentityUpdateJob"; + + public MultiDevicePniIdentityUpdateJob() { + this(new Parameters.Builder() + .setQueue("__MULTI_DEVICE_PNI_IDENTITY_UPDATE_JOB__") + .setMaxInstancesForFactory(1) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build()); + } + + private MultiDevicePniIdentityUpdateJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException, UntrustedIdentityException { + if (!Recipient.self().isRegistered()) { + throw new NotPushRegisteredException(); + } + + if (!TextSecurePreferences.isMultiDevice(context)) { + Log.i(TAG, "Not multi device, aborting..."); + return; + } + + if (SignalStore.account().isLinkedDevice()) { + Log.i(TAG, "Not primary device, aborting..."); + return; + } + + IdentityKeyPair pniIdentityKeyPair = SignalStore.account().getPniIdentityKey(); + SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forPniIdentity(PniIdentity.newBuilder() + .setPublicKey(ByteString.copyFrom(pniIdentityKeyPair.getPublicKey().serialize())) + .setPrivateKey(ByteString.copyFrom(pniIdentityKeyPair.getPrivateKey().serialize())) + .build()); + + ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(syncMessage, UnidentifiedAccessUtil.getAccessForSync(context)); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + if (e instanceof ServerRejectedException) return false; + return e instanceof PushNetworkException; + } + + @Override + public void onFailure() { + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull MultiDevicePniIdentityUpdateJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new MultiDevicePniIdentityUpdateJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 342c46c646..a519f56904 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -174,7 +174,7 @@ public abstract class PushSendJob extends SendJob { return Optional.absent(); } - return Optional.of(ProfileKeyUtil.getProfileKey(context)); + return Optional.of(ProfileKeyUtil.getSelfProfileKey().serialize()); } protected SignalServiceAttachment getAttachmentFor(Attachment attachment) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 2342e3d801..9a950ab1ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -396,7 +396,7 @@ public class RetrieveProfileJob extends BaseJob { IdentityKey identityKey = new IdentityKey(Base64.decode(identityKeyValue), 0); - if (!ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient).isPresent()) { + if (!ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.getId()).isPresent()) { Log.w(TAG, "Still first use for " + recipient.getId()); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index bd5dde4a56..cef473735c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -68,6 +68,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob; +import org.thoughtcrime.securesms.jobs.MultiDevicePniIdentityUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob; import org.thoughtcrime.securesms.jobs.NullMessageSendJob; import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob; @@ -1237,6 +1238,10 @@ public final class MessageContentProcessor { if (message.isKeysRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceKeysUpdateJob()); } + + if (message.isPniIdentityRequest()) { + ApplicationDependencies.getJobManager().add(new MultiDevicePniIdentityUpdateJob()); + } } private void handleSynchronizeReadMessage(@NonNull List readMessages, long envelopeTimestamp, @NonNull Recipient senderRecipient) diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java b/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java index 72a627d218..cc0fa270c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.util.FeatureFlags; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; public class AccountManagerFactory { @@ -20,6 +21,7 @@ public class AccountManagerFactory { public static @NonNull SignalServiceAccountManager createAuthenticated(@NonNull Context context, @NonNull ACI aci, + @NonNull PNI pni, @NonNull String number, int deviceId, @NonNull String password) @@ -36,6 +38,7 @@ public class AccountManagerFactory { return new SignalServiceAccountManager(ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number), aci, + pni, number, deviceId, password, @@ -44,7 +47,7 @@ public class AccountManagerFactory { } /** - * Should only be used during registration when you haven't yet been assigned a UUID. + * Should only be used during registration when you haven't yet been assigned an ACI. */ public static @NonNull SignalServiceAccountManager createUnauthenticated(@NonNull Context context, @NonNull String number, @@ -62,7 +65,7 @@ public class AccountManagerFactory { } return new SignalServiceAccountManager(new SignalServiceNetworkAccess(context).getConfiguration(number), - null, number, deviceId, password, BuildConfig.SIGNAL_AGENT, FeatureFlags.okHttpAutomaticRetry()); + null, null, number, deviceId, password, BuildConfig.SIGNAL_AGENT, FeatureFlags.okHttpAutomaticRetry()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index cc535c50cc..8e59d307d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -138,7 +138,7 @@ public final class RegistrationRepository { ApplicationDependencies.getProtocolStore().pni().sessions().archiveAllSessions(); SenderKeyUtil.clearAllState(context); - SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); + SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, aci, pni, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); SignalServiceAccountDataStoreImpl aciProtocolStore = ApplicationDependencies.getProtocolStore().aci(); SignalServiceAccountDataStoreImpl pniProtocolStore = ApplicationDependencies.getProtocolStore().pni(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipher.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipher.kt index 5bf22f33d0..0947d5dd92 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipher.kt @@ -67,9 +67,9 @@ class SecondaryProvisioningCipher private constructor(private val secondaryIdent val provisioningMessage = ProvisioningProtos.ProvisionMessage.parseFrom(plaintext) return ProvisionDecryptResult.Success( - uuid = UuidUtil.parseOrThrow(provisioningMessage.uuid), + uuid = UuidUtil.parseOrThrow(provisioningMessage.aci), e164 = provisioningMessage.number, - identityKeyPair = IdentityKeyPair(IdentityKey(provisioningMessage.identityKeyPublic.toByteArray()), Curve.decodePrivatePoint(provisioningMessage.identityKeyPrivate.toByteArray())), + identityKeyPair = IdentityKeyPair(IdentityKey(provisioningMessage.aciIdentityKeyPublic.toByteArray()), Curve.decodePrivatePoint(provisioningMessage.aciIdentityKeyPrivate.toByteArray())), profileKey = ProfileKey(provisioningMessage.profileKey.toByteArray()), areReadReceiptsEnabled = provisioningMessage.readReceipts, primaryUserAgent = provisioningMessage.userAgent, diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipherTest.kt b/app/src/test/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipherTest.kt index f4aaf24fa2..6a7b07d4f7 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipherTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/secondary/SecondaryProvisioningCipherTest.kt @@ -24,12 +24,12 @@ class SecondaryProvisioningCipherTest { val primaryProvisioningCipher = PrimaryProvisioningCipher(provisioningCipher.secondaryDevicePublicKey.publicKey) val message = ProvisionMessage.newBuilder() - .setIdentityKeyPublic(ByteString.copyFrom(primaryIdentityKeyPair.publicKey.serialize())) - .setIdentityKeyPrivate(ByteString.copyFrom(primaryIdentityKeyPair.privateKey.serialize())) + .setAciIdentityKeyPublic(ByteString.copyFrom(primaryIdentityKeyPair.publicKey.serialize())) + .setAciIdentityKeyPrivate(ByteString.copyFrom(primaryIdentityKeyPair.privateKey.serialize())) .setProvisioningCode("code") .setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE) .setNumber("+14045555555") - .setUuid(UUID.randomUUID().toString()) + .setAci(UUID.randomUUID().toString()) .setProfileKey(ByteString.copyFrom(primaryProfileKey.serialize())) val provisionMessage = ProvisioningProtos.ProvisionEnvelope.parseFrom(primaryProvisioningCipher.encrypt(message.build())) @@ -39,7 +39,7 @@ class SecondaryProvisioningCipherTest { val success = result as SecondaryProvisioningCipher.ProvisionDecryptResult.Success - assertThat(success.uuid.toString(), `is`(message.uuid)) + assertThat(success.uuid.toString(), `is`(message.aci)) assertThat(success.e164, `is`(message.number)) assertThat(success.identityKeyPair.serialize(), `is`(primaryIdentityKeyPair.serialize())) assertThat(success.profileKey.serialize(), `is`(primaryProfileKey.serialize())) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 3af6f365a3..ce9a54ca47 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -19,6 +19,7 @@ import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.libsignal.util.guava.Preconditions; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; @@ -35,6 +36,7 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.AccountIdentifier; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.exceptions.NoContentException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; @@ -128,14 +130,16 @@ public class SignalServiceAccountManager { /** * Construct a SignalServiceAccountManager. - * @param configuration The URL for the Signal Service. - * @param aci The Signal Service UUID. + * @param configuration The URL for the Signal Service. + * @param aci The Signal Service ACI. + * @param pni The Signal Service PNI. * @param e164 The Signal Service phone number. * @param password A Signal Service password. * @param signalAgent A string which identifies the client software. */ public SignalServiceAccountManager(SignalServiceConfiguration configuration, ACI aci, + PNI pni, String e164, int deviceId, String password, @@ -143,7 +147,7 @@ public class SignalServiceAccountManager { boolean automaticNetworkRetry) { this(configuration, - new StaticCredentialsProvider(aci, e164, deviceId, password), + new StaticCredentialsProvider(aci, pni, e164, deviceId, password), signalAgent, new GroupsV2Operations(ClientZkOperations.create(configuration)), automaticNetworkRetry); @@ -715,36 +719,32 @@ public class SignalServiceAccountManager { public void addDevice(String deviceIdentifier, ECPublicKey deviceKey, - IdentityKeyPair identityKeyPair, - Optional profileKey, + IdentityKeyPair aciIdentityKeyPair, + IdentityKeyPair pniIdentityKeyPair, + ProfileKey profileKey, String code) throws InvalidKeyException, IOException { - PrimaryProvisioningCipher cipher = new PrimaryProvisioningCipher(deviceKey); - ProvisionMessage.Builder message = ProvisionMessage.newBuilder() - .setIdentityKeyPublic(ByteString.copyFrom(identityKeyPair.getPublicKey().serialize())) - .setIdentityKeyPrivate(ByteString.copyFrom(identityKeyPair.getPrivateKey().serialize())) - .setProvisioningCode(code) - .setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE); - String e164 = credentials.getE164(); ACI aci = credentials.getAci(); + PNI pni = credentials.getPni(); - if (e164 != null) { - message.setNumber(e164); - } else { - throw new AssertionError("Missing phone number!"); - } + Preconditions.checkArgument(e164 != null, "Missing e164!"); + Preconditions.checkArgument(aci != null, "Missing ACI!"); + Preconditions.checkArgument(pni != null, "Missing PNI!"); - if (aci != null) { - message.setUuid(aci.toString()); - } else { - Log.w(TAG, "[addDevice] Missing UUID."); - } - - if (profileKey.isPresent()) { - message.setProfileKey(ByteString.copyFrom(profileKey.get())); - } + PrimaryProvisioningCipher cipher = new PrimaryProvisioningCipher(deviceKey); + ProvisionMessage.Builder message = ProvisionMessage.newBuilder() + .setAciIdentityKeyPublic(ByteString.copyFrom(aciIdentityKeyPair.getPublicKey().serialize())) + .setAciIdentityKeyPrivate(ByteString.copyFrom(aciIdentityKeyPair.getPrivateKey().serialize())) + .setPniIdentityKeyPublic(ByteString.copyFrom(pniIdentityKeyPair.getPublicKey().serialize())) + .setPniIdentityKeyPrivate(ByteString.copyFrom(pniIdentityKeyPair.getPrivateKey().serialize())) + .setAci(aci.toString()) + .setPni(pni.toString()) + .setNumber(e164) + .setProfileKey(ByteString.copyFrom(profileKey.serialize())) + .setProvisioningCode(code) + .setProvisioningVersion(ProvisioningVersion.CURRENT_VALUE); byte[] ciphertext = cipher.encrypt(message.build()); this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/RequestMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/RequestMessage.java index c2f931a33d..db39489328 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/RequestMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/RequestMessage.java @@ -43,4 +43,8 @@ public class RequestMessage { public boolean isKeysRequest() { return request.getType() == Request.Type.KEYS; } + + public boolean isPniIdentityRequest() { + return request.getType() == Request.Type.PNI_IDENTITY; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java index b6759c59ea..3db0283fca 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/SignalServiceSyncMessage.java @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.messages.multidevice; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.PniIdentity; import java.util.LinkedList; import java.util.List; @@ -28,6 +29,7 @@ public class SignalServiceSyncMessage { private final Optional keys; private final Optional messageRequestResponse; private final Optional outgoingPaymentMessage; + private final Optional pniIdentity; private final Optional> views; private SignalServiceSyncMessage(Optional sent, @@ -44,7 +46,8 @@ public class SignalServiceSyncMessage { Optional keys, Optional messageRequestResponse, Optional outgoingPaymentMessage, - Optional> views) + Optional> views, + Optional pniIdentity) { this.sent = sent; this.contacts = contacts; @@ -61,6 +64,7 @@ public class SignalServiceSyncMessage { this.messageRequestResponse = messageRequestResponse; this.outgoingPaymentMessage = outgoingPaymentMessage; this.views = views; + this.pniIdentity = pniIdentity; } public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) { @@ -78,6 +82,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -96,6 +101,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -114,6 +120,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -132,6 +139,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -150,6 +158,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -168,7 +177,8 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), - Optional.of(views)); + Optional.of(views), + Optional.absent()); } public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) { @@ -186,6 +196,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -207,6 +218,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -225,6 +237,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -243,6 +256,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -261,6 +275,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -279,6 +294,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -297,6 +313,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -315,6 +332,7 @@ public class SignalServiceSyncMessage { Optional.of(keys), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -333,6 +351,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.of(messageRequestResponse), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -351,9 +370,29 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.of(outgoingPaymentMessage), + Optional.absent(), Optional.absent()); } + public static SignalServiceSyncMessage forPniIdentity(PniIdentity pniIdentity) { + return new SignalServiceSyncMessage(Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.of(pniIdentity)); + } + public static SignalServiceSyncMessage empty() { return new SignalServiceSyncMessage(Optional.absent(), Optional.absent(), @@ -369,6 +408,7 @@ public class SignalServiceSyncMessage { Optional.absent(), Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent()); } @@ -432,6 +472,10 @@ public class SignalServiceSyncMessage { return views; } + public Optional getPniIdentity() { + return pniIdentity; + } + public enum FetchType { LOCAL_PROFILE, STORAGE_MANIFEST, diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/CredentialsProvider.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/CredentialsProvider.java index 0a24ca5b85..256f4aeae3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/CredentialsProvider.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/CredentialsProvider.java @@ -7,9 +7,11 @@ package org.whispersystems.signalservice.api.util; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; public interface CredentialsProvider { ACI getAci(); + PNI getPni(); String getE164(); int getDeviceId(); String getPassword(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/StaticCredentialsProvider.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/StaticCredentialsProvider.java index 5662602715..4a211f9f6c 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/StaticCredentialsProvider.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/util/StaticCredentialsProvider.java @@ -7,17 +7,20 @@ package org.whispersystems.signalservice.internal.util; import org.whispersystems.signalservice.api.push.ACI; +import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.util.CredentialsProvider; public class StaticCredentialsProvider implements CredentialsProvider { private final ACI aci; + private final PNI pni; private final String e164; - private final int deviceId; + private final int deviceId; private final String password; - public StaticCredentialsProvider(ACI aci, String e164, int deviceId, String password) { + public StaticCredentialsProvider(ACI aci, PNI pni, String e164, int deviceId, String password) { this.aci = aci; + this.pni = pni; this.e164 = e164; this.deviceId = deviceId; this.password = password; @@ -28,6 +31,11 @@ public class StaticCredentialsProvider implements CredentialsProvider { return aci; } + @Override + public PNI getPni() { + return pni; + } + @Override public String getE164() { return e164; diff --git a/libsignal/service/src/main/proto/Provisioning.proto b/libsignal/service/src/main/proto/Provisioning.proto index 80de36a693..61b97019e9 100644 --- a/libsignal/service/src/main/proto/Provisioning.proto +++ b/libsignal/service/src/main/proto/Provisioning.proto @@ -20,15 +20,19 @@ message ProvisionEnvelope { } message ProvisionMessage { - optional bytes identityKeyPublic = 1; - optional bytes identityKeyPrivate = 2; - optional string number = 3; - optional string uuid = 8; - optional string provisioningCode = 4; - optional string userAgent = 5; - optional bytes profileKey = 6; - optional bool readReceipts = 7; - optional uint32 provisioningVersion = 9; + optional bytes aciIdentityKeyPublic = 1; + optional bytes aciIdentityKeyPrivate = 2; + optional bytes pniIdentityKeyPublic = 11; + optional bytes pniIdentityKeyPrivate = 12; + optional string aci = 8; + optional string pni = 10; + optional string number = 3; + optional string provisioningCode = 4; + optional string userAgent = 5; + optional bytes profileKey = 6; + optional bool readReceipts = 7; + optional uint32 provisioningVersion = 9; + // NEXT ID: 13 } enum ProvisioningVersion { diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 889ec66aa6..fa2270199e 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -26,12 +26,14 @@ message Envelope { optional string sourceE164 = 2; optional string sourceUuid = 11; optional uint32 sourceDevice = 7; + optional string destinationUuid = 13; optional string relay = 3; optional uint64 timestamp = 5; optional bytes legacyMessage = 6; // Contains an encrypted DataMessage optional bytes content = 8; // Contains an encrypted Content optional string serverGuid = 9; optional uint64 serverTimestamp = 10; + // NEXT ID: 14 } message Content { @@ -408,6 +410,7 @@ message SyncMessage { BLOCKED = 3; CONFIGURATION = 4; KEYS = 5; + PNI_IDENTITY = 6; } optional Type type = 1; @@ -466,6 +469,11 @@ message SyncMessage { optional bytes storageService = 1; } + message PniIdentity { + optional bytes publicKey = 1; + optional bytes privateKey = 2; + } + message MessageRequestResponse { enum Type { UNKNOWN = 0;