Use separate PNI key distribution endpoint instead of change number.

This commit is contained in:
Clark
2023-06-15 10:51:52 -04:00
committed by Cody Henthorne
parent 3d4875bcfe
commit 186a93f5d1
7 changed files with 228 additions and 31 deletions

View File

@@ -113,7 +113,7 @@ class ChangeNumberRepository(
.timeout(15, TimeUnit.SECONDS)
}
fun changeNumber(sessionId: String? = null, recoveryPassword: String? = null, newE164: String, pniUpdateMode: Boolean = false): Single<ServiceResponse<VerifyResponse>> {
fun changeNumber(sessionId: String? = null, recoveryPassword: String? = null, newE164: String): Single<ServiceResponse<VerifyResponse>> {
check((sessionId != null && recoveryPassword == null) || (sessionId == null && recoveryPassword != null))
return Single.fromCallable {
@@ -125,8 +125,7 @@ class ChangeNumberRepository(
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
sessionId = sessionId,
recoveryPassword = recoveryPassword,
newE164 = newE164,
pniUpdateMode = pniUpdateMode
newE164 = newE164
)
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
@@ -308,19 +307,17 @@ class ChangeNumberRepository(
}.subscribeOn(Schedulers.single())
}
@Suppress("UsePropertyAccessSyntax")
@WorkerThread
private fun createChangeNumberRequest(
sessionId: String? = null,
recoveryPassword: String? = null,
newE164: String,
registrationLock: String? = null,
pniUpdateMode: Boolean = false
registrationLock: String? = null
): ChangeNumberRequestData {
val selfIdentifier: String = SignalStore.account().requireAci().toString()
val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci()
val pniIdentity: IdentityKeyPair = if (pniUpdateMode) SignalStore.account().pniIdentityKey else IdentityKeyUtil.generateIdentityKeyPair()
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
val deviceMessages = mutableListOf<OutgoingPushMessage>()
val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
val devicePniLastResortKyberPreKeys = mutableMapOf<Int, KyberPreKeyEntity>()
@@ -334,11 +331,7 @@ class ChangeNumberRepository(
.forEach { deviceId ->
// Signed Prekeys
val signedPreKeyRecord: SignedPreKeyRecord = if (deviceId == primaryDeviceId) {
if (pniUpdateMode) {
ApplicationDependencies.getProtocolStore().pni().loadSignedPreKey(SignalStore.account().pniPreKeys.activeSignedPreKeyId)
} else {
PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
}
PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
} else {
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
}
@@ -346,22 +339,14 @@ class ChangeNumberRepository(
// Last-resort kyber prekeys
val lastResortKyberPreKeyRecord: KyberPreKeyRecord = if (deviceId == primaryDeviceId) {
if (pniUpdateMode) {
ApplicationDependencies.getProtocolStore().pni().loadKyberPreKey(SignalStore.account().pniPreKeys.lastResortKyberPreKeyId)
} else {
PreKeyUtil.generateAndStoreLastResortKyberPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
}
PreKeyUtil.generateAndStoreLastResortKyberPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
} else {
PreKeyUtil.generateKyberPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
}
devicePniLastResortKyberPreKeys[deviceId] = KyberPreKeyEntity(lastResortKyberPreKeyRecord.id, lastResortKyberPreKeyRecord.keyPair.publicKey, lastResortKyberPreKeyRecord.signature)
// Registration Ids
var pniRegistrationId = if (deviceId == primaryDeviceId && pniUpdateMode) {
SignalStore.account().pniRegistrationId
} else {
-1
}
var pniRegistrationId = -1
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
pniRegistrationId = KeyHelper.generateRegistrationId(false)
@@ -378,7 +363,7 @@ class ChangeNumberRepository(
.setNewE164(newE164)
.build()
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(deviceId, pniChangeNumber)
deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber)
}
}

View File

@@ -568,8 +568,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(DSLSettingsText.from("PNP"))
clickPref(
title = DSLSettingsText.from("Trigger No-Op Change Number"),
summary = DSLSettingsText.from("Mimics the 'Hello world' event"),
title = DSLSettingsText.from("Trigger 'Hello World' event"),
isEnabled = true,
onClick = {
SimpleTask.run(viewLifecycleOwner.lifecycle, {

View File

@@ -1,17 +1,41 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.safeBlockingGet
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
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.ChangeNumberRepository
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
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.VerifyResponse
import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
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.ServiceResponse
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
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.
@@ -23,7 +47,6 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
companion object {
const val KEY = "PnpInitializeDevicesJob"
private val TAG = Log.tag(PnpInitializeDevicesJob::class.java)
private const val PLACEHOLDER_SESSION_ID = "123456789"
@JvmStatic
fun enqueueIfNecessary() {
@@ -81,13 +104,11 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
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(sessionId = PLACEHOLDER_SESSION_ID, newE164 = e164, pniUpdateMode = true)
Log.i(TAG, "Initializing PNI for linked devices")
initializeDevices(e164)
.map(::VerifyResponseWithoutKbs)
.safeBlockingGet()
.resultOrThrow
@@ -104,6 +125,109 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base
}
}
private fun initializeDevices(newE164: String): Single<ServiceResponse<VerifyResponse>> {
val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
val messageSender = ApplicationDependencies.getSignalServiceMessageSender()
return Single.fromCallable {
var completed = false
var attempts = 0
lateinit var distributionResponse: ServiceResponse<VerifyAccountResponse>
while (!completed && attempts < 5) {
val request = createInitializeDevicesRequest(
newE164 = newE164
)
distributionResponse = accountManager.distributePniKeys(request)
val possibleError: Throwable? = distributionResponse.applicationError.orNull()
if (possibleError is MismatchedDevicesException) {
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
attempts++
} else {
completed = true
}
}
VerifyResponse.from(distributionResponse, null, null)
}.subscribeOn(Schedulers.single())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
}
@WorkerThread
private fun createInitializeDevicesRequest(
newE164: String
): PniKeyDistributionRequest {
val selfIdentifier: String = SignalStore.account().requireAci().toString()
val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci()
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val messageSender = ApplicationDependencies.getSignalServiceMessageSender()
val pniIdentity: IdentityKeyPair = SignalStore.account().pniIdentityKey
val deviceMessages = mutableListOf<OutgoingPushMessage>()
val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
val devicePniLastResortKyberPreKeys = mutableMapOf<Int, KyberPreKeyEntity>()
val pniRegistrationIds = mutableMapOf<Int, Int>()
val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
val devices: List<Int> = 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.generateKyberPreKey(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 = SignalServiceProtos.SyncMessage.PniChangeNumber.newBuilder()
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
.setLastResortKyberPreKey(lastResortKyberPreKeyRecord.serialize().toProtoByteString())
.setRegistrationId(pniRegistrationId)
.setNewE164(newE164)
.build()
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
}

View File

@@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
@@ -651,6 +652,15 @@ public class SignalServiceAccountManager {
this.pushServiceSocket.sendProvisioningMessage(deviceIdentifier, ciphertext);
}
public ServiceResponse<VerifyAccountResponse> distributePniKeys(PniKeyDistributionRequest request) {
try {
VerifyAccountResponse response = this.pushServiceSocket.distributePniKeys(request);
return ServiceResponse.forResult(response, 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}
public List<DeviceInfo> getDevices() throws IOException {
return this.pushServiceSocket.getDevices();
}

View File

@@ -708,7 +708,7 @@ public class SignalServiceMessageSender {
* @param pniChangeNumber - Linked device specific updated PNI details
* @return Encrypted {@link OutgoingPushMessage} to be included in the change number request sent to the server
*/
public @Nonnull OutgoingPushMessage getEncryptedSyncPniChangeNumberMessage(int deviceId, @Nonnull SyncMessage.PniChangeNumber pniChangeNumber)
public @Nonnull OutgoingPushMessage getEncryptedSyncPniInitializeDeviceMessage(int deviceId, @Nonnull SyncMessage.PniChangeNumber pniChangeNumber)
throws UntrustedIdentityException, IOException, InvalidKeyException
{
SyncMessage.Builder syncMessage = createSyncMessageBuilder().setPniChangeNumber(pniChangeNumber);

View File

@@ -0,0 +1,70 @@
package org.whispersystems.signalservice.api.account;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.libsignal.protocol.IdentityKey;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import java.util.List;
import java.util.Map;
public final class PniKeyDistributionRequest {
@JsonProperty
@JsonSerialize(using = JsonUtil.IdentityKeySerializer.class)
@JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class)
private IdentityKey pniIdentityKey;
@JsonProperty
private List<OutgoingPushMessage> deviceMessages;
@JsonProperty
private Map<String, SignedPreKeyEntity> devicePniSignedPrekeys;
@JsonProperty("devicePniPqLastResortPrekeys")
private Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys;
@JsonProperty
private Map<String, Integer> pniRegistrationIds;
@JsonProperty
private boolean signatureValidOnEachSignedPreKey;
@SuppressWarnings("unused")
public PniKeyDistributionRequest() {}
public PniKeyDistributionRequest(IdentityKey pniIdentityKey,
List<OutgoingPushMessage> deviceMessages,
Map<String, SignedPreKeyEntity> devicePniSignedPrekeys,
Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys,
Map<String, Integer> pniRegistrationIds,
boolean signatureValidOnEachSignedPreKey)
{
this.pniIdentityKey = pniIdentityKey;
this.deviceMessages = deviceMessages;
this.devicePniSignedPrekeys = devicePniSignedPrekeys;
this.devicePniLastResortKyberPrekeys = devicePniLastResortKyberPrekeys;
this.pniRegistrationIds = pniRegistrationIds;
this.signatureValidOnEachSignedPreKey = signatureValidOnEachSignedPreKey;
}
public IdentityKey getPniIdentityKey() {
return pniIdentityKey;
}
public List<OutgoingPushMessage> getDeviceMessages() {
return deviceMessages;
}
public Map<String, SignedPreKeyEntity> getDevicePniSignedPrekeys() {
return devicePniSignedPrekeys;
}
public Map<String, Integer> getPniRegistrationIds() {
return pniRegistrationIds;
}
}

View File

@@ -40,6 +40,7 @@ import org.signal.storageservice.protos.groups.GroupExternalCredential;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.whispersystems.signalservice.api.account.AccountAttributes;
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest;
import org.whispersystems.signalservice.api.account.PreKeyUpload;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
@@ -216,6 +217,7 @@ public class PushServiceSocket {
private static final String CHANGE_NUMBER_PATH = "/v2/accounts/number";
private static final String IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s";
private static final String REQUEST_ACCOUNT_DATA_PATH = "/v2/accounts/data_report";
private static final String PNI_KEY_DISTRUBTION_PATH = "/v2/accounts/phone_number_identity_key_distribution";
private static final String PREKEY_METADATA_PATH = "/v2/keys?identity=%s";
private static final String PREKEY_PATH = "/v2/keys?identity=%s";
@@ -448,6 +450,13 @@ public class PushServiceSocket {
return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class);
}
public VerifyAccountResponse distributePniKeys(@NonNull PniKeyDistributionRequest distributionRequest) throws IOException {
String request = JsonUtil.toJson(distributionRequest);
String response = makeServiceRequest(PNI_KEY_DISTRUBTION_PATH, "PUT", request);
return JsonUtil.fromJson(response, VerifyAccountResponse.class);
}
public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes)
throws IOException
{