mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 18:26:57 +00:00
Add support for PNI registration ids and PNP change number.
This commit is contained in:
@@ -21,6 +21,7 @@ import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
@@ -329,7 +330,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getIncomingMessageObserver();
|
||||
}
|
||||
|
||||
private void initializeAppDependencies() {
|
||||
@VisibleForTesting
|
||||
void initializeAppDependencies() {
|
||||
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
setContentView(R.layout.activity_change_number_lock)
|
||||
|
||||
changeNumberRepository = ChangeNumberRepository(applicationContext)
|
||||
changeNumberRepository = ChangeNumberRepository()
|
||||
checkWhoAmI()
|
||||
}
|
||||
|
||||
@@ -50,25 +50,25 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
override fun onBackPressed() = Unit
|
||||
|
||||
private fun checkWhoAmI() {
|
||||
disposables.add(
|
||||
changeNumberRepository.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
.map { true }
|
||||
}
|
||||
disposables += changeNumberRepository
|
||||
.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni))
|
||||
.map { true }
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
|
||||
}
|
||||
|
||||
private fun onChangeStatusConfirmed() {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.state.SignalProtocolStore
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -17,22 +27,45 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
|
||||
|
||||
class ChangeNumberRepository(private val context: Context) {
|
||||
class ChangeNumberRepository(private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()) {
|
||||
|
||||
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return Completable.create { emitter ->
|
||||
ApplicationDependencies
|
||||
.getIncomingMessageObserver()
|
||||
.addDecryptionDrainedListener {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
|
||||
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
return Single.fromCallable {
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null)
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
accountManager.changeNumber(request)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
fun changeNumber(
|
||||
@@ -45,8 +78,11 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
try {
|
||||
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
|
||||
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
|
||||
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock)
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
|
||||
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
|
||||
|
||||
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(request)
|
||||
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
@@ -56,6 +92,7 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
ServiceResponse.forExecutionError(e)
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@@ -86,6 +123,49 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
SignalStore.account().setE164(e164)
|
||||
SignalStore.account().setPni(pni)
|
||||
|
||||
ApplicationDependencies.getGroupsV2Authorization().clear()
|
||||
|
||||
val metadata: PendingChangeNumberMetadata? = SignalStore.misc().pendingChangeNumberMetadata
|
||||
if (metadata == null) {
|
||||
Log.w(TAG, "No change number metadata, this shouldn't happen")
|
||||
throw AssertionError("No change number metadata")
|
||||
}
|
||||
|
||||
val originalPni = ServiceId.fromByteString(metadata.previousPni)
|
||||
|
||||
if (originalPni == pni) {
|
||||
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
|
||||
} else {
|
||||
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
|
||||
val pniRegistrationId = metadata.pniRegistrationId
|
||||
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
|
||||
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
SignalStore.account().pniRegistrationId = pniRegistrationId
|
||||
SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
|
||||
|
||||
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
|
||||
val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(pniProtocolStore, pniMetadataStore)
|
||||
|
||||
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
|
||||
accountManager.setPreKeys(ServiceIdType.PNI, pniProtocolStore.identityKeyPair.publicKey, signedPreKey, oneTimePreKeys)
|
||||
pniMetadataStore.isSignedPreKeyRegistered = true
|
||||
|
||||
pniProtocolStore.identities().saveIdentityWithoutSideEffects(
|
||||
Recipient.self().id,
|
||||
pniProtocolStore.identityKeyPair.publicKey,
|
||||
IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true,
|
||||
System.currentTimeMillis(),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
Recipient.self().live().refresh()
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
@@ -112,4 +192,66 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
@WorkerThread
|
||||
private fun createChangeNumberRequest(
|
||||
code: String,
|
||||
newE164: String,
|
||||
registrationLock: String?
|
||||
): ChangeNumberRequestData {
|
||||
val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
|
||||
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore: PreKeyMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
val devices: List<DeviceInfo> = accountManager.getDevices()
|
||||
|
||||
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
val deviceMessages = mutableListOf<OutgoingPushMessage>()
|
||||
val devicePniSignedPreKeys = mutableMapOf<String, SignedPreKeyEntity>()
|
||||
val pniRegistrationIds = mutableMapOf<String, Int>()
|
||||
val primaryDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID.toString()
|
||||
|
||||
for (device in devices) {
|
||||
val deviceId = device.id.toString()
|
||||
|
||||
// Signed Prekeys
|
||||
val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
|
||||
PreKeyUtil.generateAndStoreSignedPreKey(pniProtocolStore, pniMetadataStore, pniIdentity.privateKey, false)
|
||||
} else {
|
||||
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
|
||||
}
|
||||
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
|
||||
|
||||
// Registration Ids
|
||||
var pniRegistrationId = -1
|
||||
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false)
|
||||
}
|
||||
pniRegistrationIds[deviceId] = pniRegistrationId
|
||||
|
||||
// Device Messages
|
||||
if (deviceId != primaryDeviceId) {
|
||||
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
|
||||
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
|
||||
.setRegistrationId(pniRegistrationId)
|
||||
.build()
|
||||
|
||||
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(device.id, pniChangeNumber)
|
||||
}
|
||||
}
|
||||
|
||||
val request = ChangePhoneNumberRequest(newE164, code, registrationLock, pniIdentity.publicKey, deviceMessages, devicePniSignedPreKeys, pniRegistrationIds)
|
||||
val metadata = PendingChangeNumberMetadata.newBuilder()
|
||||
.setPreviousPni(SignalStore.account().pni!!.toByteString())
|
||||
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
|
||||
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
|
||||
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
|
||||
.build()
|
||||
|
||||
return ChangeNumberRequestData(request, metadata)
|
||||
}
|
||||
|
||||
data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
|
||||
}
|
||||
|
||||
@@ -48,29 +48,29 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
}
|
||||
|
||||
private fun requestCode() {
|
||||
lifecycleDisposable.add(
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
lifecycleDisposable += viewModel
|
||||
.ensureDecryptionsDrained()
|
||||
.andThen(viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { processor ->
|
||||
if (processor.hasResult()) {
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.localRateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to local rate limit")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired()) {
|
||||
Log.i(TAG, "Unable to request sms code due to captcha required")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
|
||||
requestingCaptcha = true
|
||||
} else if (processor.rateLimit()) {
|
||||
Log.i(TAG, "Unable to request sms code due to rate limit")
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
} else {
|
||||
Log.w(TAG, "Unable to request sms code", processor.error)
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -107,6 +108,10 @@ class ChangeNumberViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun ensureDecryptionsDrained(): Completable {
|
||||
return changeNumberRepository.ensureDecryptionsDrained()
|
||||
}
|
||||
|
||||
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
|
||||
return super.verifyCodeWithoutRegistrationLock(code)
|
||||
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
|
||||
@@ -122,6 +127,7 @@ class ChangeNumberViewModel(
|
||||
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
|
||||
return if (processor.hasResult() || processor.isServerSentError()) {
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
Single.just(processor)
|
||||
} else {
|
||||
changeNumberRepository.whoAmI()
|
||||
@@ -129,6 +135,7 @@ class ChangeNumberViewModel(
|
||||
if (Objects.equals(whoAmI.number, localNumber)) {
|
||||
Log.i(TAG, "Local and remote numbers match, we can unlock.")
|
||||
SignalStore.misc().unlockChangeNumber()
|
||||
SignalStore.misc().clearPendingChangeNumberMetadata()
|
||||
}
|
||||
processor
|
||||
}
|
||||
@@ -172,7 +179,7 @@ class ChangeNumberViewModel(
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
changeNumberRepository = ChangeNumberRepository(context),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = handle,
|
||||
password = password,
|
||||
verifyAccountRepository = VerifyAccountRepository(context),
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidKeyIdException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.SignalProtocolStore;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
@@ -64,22 +65,35 @@ public class PreKeyUtil {
|
||||
}
|
||||
|
||||
public synchronized static @NonNull SignedPreKeyRecord generateAndStoreSignedPreKey(@NonNull SignalProtocolStore protocolStore, @NonNull PreKeyMetadataStore metadataStore, boolean setAsActive) {
|
||||
return generateAndStoreSignedPreKey(protocolStore, metadataStore, protocolStore.getIdentityKeyPair().getPrivateKey(), setAsActive);
|
||||
}
|
||||
|
||||
public synchronized static @NonNull SignedPreKeyRecord generateAndStoreSignedPreKey(@NonNull SignalProtocolStore protocolStore,
|
||||
@NonNull PreKeyMetadataStore metadataStore,
|
||||
@NonNull ECPrivateKey privateKey,
|
||||
boolean setAsActive)
|
||||
{
|
||||
Log.i(TAG, "Generating signed prekeys...");
|
||||
|
||||
int signedPreKeyId = metadataStore.getNextSignedPreKeyId();
|
||||
SignedPreKeyRecord record = generateSignedPreKey(signedPreKeyId, privateKey);
|
||||
|
||||
protocolStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE);
|
||||
|
||||
if (setAsActive) {
|
||||
metadataStore.setActiveSignedPreKeyId(signedPreKeyId);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
public synchronized static @NonNull SignedPreKeyRecord generateSignedPreKey(int signedPreKeyId, @NonNull ECPrivateKey privateKey) {
|
||||
try {
|
||||
int signedPreKeyId = metadataStore.getNextSignedPreKeyId();
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
byte[] signature = Curve.calculateSignature(protocolStore.getIdentityKeyPair().getPrivateKey(), keyPair.getPublicKey().serialize());
|
||||
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
byte[] signature = Curve.calculateSignature(privateKey, keyPair.getPublicKey().serialize());
|
||||
|
||||
protocolStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
metadataStore.setNextSignedPreKeyId((signedPreKeyId + 1) % Medium.MAX_VALUE);
|
||||
|
||||
if (setAsActive) {
|
||||
metadataStore.setActiveSignedPreKeyId(signedPreKeyId);
|
||||
}
|
||||
|
||||
return record;
|
||||
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
|
||||
/**
|
||||
* Collection of extensions to make working with database protos cleaner.
|
||||
*/
|
||||
|
||||
fun ByteArray.toProtoByteString(): ByteString {
|
||||
return ByteString.copyFrom(this)
|
||||
}
|
||||
|
||||
fun BodyRangeList.Builder.addStyle(style: BodyRangeList.BodyRange.Style, start: Int, length: Int): BodyRangeList.Builder {
|
||||
addRanges(
|
||||
BodyRangeList.BodyRange.newBuilder()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package org.thoughtcrime.securesms.dependencies;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.Hex;
|
||||
import org.signal.core.util.concurrent.DeadlockDetector;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
@@ -56,10 +56,12 @@ import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
import org.whispersystems.signalservice.api.services.DonationsService;
|
||||
import org.whispersystems.signalservice.api.services.ProfileService;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
@@ -77,6 +79,7 @@ import okhttp3.OkHttpClient;
|
||||
* All future application-scoped singletons should be written as normal objects, then placed here
|
||||
* to manage their singleton-ness.
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ApplicationDependencies {
|
||||
|
||||
private static final Object LOCK = new Object();
|
||||
@@ -159,7 +162,7 @@ public class ApplicationDependencies {
|
||||
|
||||
synchronized (LOCK) {
|
||||
if (accountManager == null) {
|
||||
accountManager = provider.provideSignalServiceAccountManager();
|
||||
accountManager = provider.provideSignalServiceAccountManager(getSignalServiceNetworkAccess().getConfiguration(), getGroupsV2Operations());
|
||||
}
|
||||
return accountManager;
|
||||
}
|
||||
@@ -183,7 +186,7 @@ public class ApplicationDependencies {
|
||||
if (groupsV2Operations == null) {
|
||||
synchronized (LOCK) {
|
||||
if (groupsV2Operations == null) {
|
||||
groupsV2Operations = provider.provideGroupsV2Operations();
|
||||
groupsV2Operations = provider.provideGroupsV2Operations(getSignalServiceNetworkAccess().getConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,11 +195,7 @@ public class ApplicationDependencies {
|
||||
}
|
||||
|
||||
public static @NonNull KeyBackupService getKeyBackupService(@NonNull KbsEnclave enclave) {
|
||||
return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application),
|
||||
enclave.getEnclaveName(),
|
||||
Hex.fromStringOrThrow(enclave.getServiceId()),
|
||||
enclave.getMrEnclave(),
|
||||
10);
|
||||
return provider.provideKeyBackupService(getSignalServiceAccountManager(), IasKeyStore.getIasKeyStore(application), enclave);
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2StateProcessor getGroupsV2StateProcessor() {
|
||||
@@ -220,7 +219,7 @@ public class ApplicationDependencies {
|
||||
|
||||
synchronized (LOCK) {
|
||||
if (messageSender == null) {
|
||||
messageSender = provider.provideSignalServiceMessageSender(getSignalWebSocket(), getProtocolStore());
|
||||
messageSender = provider.provideSignalServiceMessageSender(getSignalWebSocket(), getProtocolStore(), getSignalServiceNetworkAccess().getConfiguration());
|
||||
}
|
||||
return messageSender;
|
||||
}
|
||||
@@ -229,7 +228,7 @@ public class ApplicationDependencies {
|
||||
public static @NonNull SignalServiceMessageReceiver getSignalServiceMessageReceiver() {
|
||||
synchronized (LOCK) {
|
||||
if (messageReceiver == null) {
|
||||
messageReceiver = provider.provideSignalServiceMessageReceiver();
|
||||
messageReceiver = provider.provideSignalServiceMessageReceiver(getSignalServiceNetworkAccess().getConfiguration());
|
||||
}
|
||||
return messageReceiver;
|
||||
}
|
||||
@@ -571,7 +570,7 @@ public class ApplicationDependencies {
|
||||
if (signalWebSocket == null) {
|
||||
synchronized (LOCK) {
|
||||
if (signalWebSocket == null) {
|
||||
signalWebSocket = provider.provideSignalWebSocket();
|
||||
signalWebSocket = provider.provideSignalWebSocket(getSignalServiceNetworkAccess().getConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,7 +626,7 @@ public class ApplicationDependencies {
|
||||
if (donationsService == null) {
|
||||
synchronized (LOCK) {
|
||||
if (donationsService == null) {
|
||||
donationsService = provider.provideDonationsService();
|
||||
donationsService = provider.provideDonationsService(getSignalServiceNetworkAccess().getConfiguration(), getGroupsV2Operations());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,7 +650,7 @@ public class ApplicationDependencies {
|
||||
if (clientZkReceiptOperations == null) {
|
||||
synchronized (LOCK) {
|
||||
if (clientZkReceiptOperations == null) {
|
||||
clientZkReceiptOperations = provider.provideClientZkReceiptOperations();
|
||||
clientZkReceiptOperations = provider.provideClientZkReceiptOperations(getSignalServiceNetworkAccess().getConfiguration());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,10 +669,10 @@ public class ApplicationDependencies {
|
||||
}
|
||||
|
||||
public interface Provider {
|
||||
@NonNull GroupsV2Operations provideGroupsV2Operations();
|
||||
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
||||
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore);
|
||||
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver();
|
||||
@NonNull GroupsV2Operations provideGroupsV2Operations(@NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations);
|
||||
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
@NonNull SignalServiceNetworkAccess provideSignalServiceNetworkAccess();
|
||||
@NonNull IncomingMessageProcessor provideIncomingMessageProcessor();
|
||||
@NonNull BackgroundMessageRetriever provideBackgroundMessageRetriever();
|
||||
@@ -697,14 +696,15 @@ public class ApplicationDependencies {
|
||||
@NonNull SignalCallManager provideSignalCallManager();
|
||||
@NonNull PendingRetryReceiptManager providePendingRetryReceiptManager();
|
||||
@NonNull PendingRetryReceiptCache providePendingRetryReceiptCache();
|
||||
@NonNull SignalWebSocket provideSignalWebSocket();
|
||||
@NonNull SignalWebSocket provideSignalWebSocket(@NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
@NonNull SignalServiceDataStoreImpl provideProtocolStore();
|
||||
@NonNull GiphyMp4Cache provideGiphyMp4Cache();
|
||||
@NonNull SimpleExoPlayerPool provideExoPlayerPool();
|
||||
@NonNull AudioManagerCompat provideAndroidCallAudioManager();
|
||||
@NonNull DonationsService provideDonationsService();
|
||||
@NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations);
|
||||
@NonNull ProfileService provideProfileService(@NonNull ClientZkProfileOperations profileOperations, @NonNull SignalServiceMessageReceiver signalServiceMessageReceiver, @NonNull SignalWebSocket signalWebSocket);
|
||||
@NonNull DeadlockDetector provideDeadlockDetector();
|
||||
@NonNull ClientZkReceiptOperations provideClientZkReceiptOperations();
|
||||
@NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration);
|
||||
@NonNull KeyBackupService provideKeyBackupService(@NonNull SignalServiceAccountManager signalServiceAccountManager, @NonNull KeyStore keyStore, @NonNull KbsEnclave enclave);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.Hex;
|
||||
import org.signal.core.util.concurrent.DeadlockDetector;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
@@ -70,6 +73,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.video.exo.GiphyMp4Cache;
|
||||
import org.thoughtcrime.securesms.video.exo.SimpleExoPlayerPool;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
@@ -85,8 +89,10 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||
import org.whispersystems.signalservice.api.websocket.WebSocketFactory;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketConnection;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -101,45 +107,45 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private @NonNull ClientZkOperations provideClientZkOperations() {
|
||||
return ClientZkOperations.create(provideSignalServiceNetworkAccess().getConfiguration());
|
||||
private @NonNull ClientZkOperations provideClientZkOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
return ClientZkOperations.create(signalServiceConfiguration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GroupsV2Operations provideGroupsV2Operations() {
|
||||
return new GroupsV2Operations(provideClientZkOperations(), FeatureFlags.groupLimits().getHardLimit());
|
||||
public @NonNull GroupsV2Operations provideGroupsV2Operations(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
return new GroupsV2Operations(provideClientZkOperations(signalServiceConfiguration), FeatureFlags.groupLimits().getHardLimit());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() {
|
||||
return new SignalServiceAccountManager(provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
|
||||
return new SignalServiceAccountManager(signalServiceConfiguration,
|
||||
new DynamicCredentialsProvider(),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
provideGroupsV2Operations(),
|
||||
groupsV2Operations,
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore) {
|
||||
return new SignalServiceMessageSender(provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
return new SignalServiceMessageSender(signalServiceConfiguration,
|
||||
new DynamicCredentialsProvider(),
|
||||
protocolStore,
|
||||
ReentrantSessionLock.INSTANCE,
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
signalWebSocket,
|
||||
Optional.of(new SecurityEventListener(context)),
|
||||
provideClientZkOperations().getProfileOperations(),
|
||||
provideGroupsV2Operations(signalServiceConfiguration).getProfileOperations(),
|
||||
SignalExecutors.newCachedBoundedExecutor("signal-messages", 1, 16, 30),
|
||||
ByteUnit.KILOBYTES.toBytes(256),
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver() {
|
||||
return new SignalServiceMessageReceiver(provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
return new SignalServiceMessageReceiver(signalServiceConfiguration,
|
||||
new DynamicCredentialsProvider(),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
provideClientZkOperations().getProfileOperations(),
|
||||
provideGroupsV2Operations(signalServiceConfiguration).getProfileOperations(),
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@@ -275,10 +281,10 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull SignalWebSocket provideSignalWebSocket() {
|
||||
public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
SleepTimer sleepTimer = SignalStore.account().isFcmEnabled() ? new UptimeSleepTimer() : new AlarmSleepTimer(context);
|
||||
SignalWebSocketHealthMonitor healthMonitor = new SignalWebSocketHealthMonitor(context, sleepTimer);
|
||||
SignalWebSocket signalWebSocket = new SignalWebSocket(provideWebSocketFactory(healthMonitor));
|
||||
SignalWebSocket signalWebSocket = new SignalWebSocket(provideWebSocketFactory(signalServiceConfiguration, healthMonitor));
|
||||
|
||||
healthMonitor.monitor(signalWebSocket);
|
||||
|
||||
@@ -346,11 +352,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull DonationsService provideDonationsService() {
|
||||
return new DonationsService(provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
public @NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) {
|
||||
return new DonationsService(signalServiceConfiguration,
|
||||
new DynamicCredentialsProvider(),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
provideGroupsV2Operations(),
|
||||
groupsV2Operations,
|
||||
FeatureFlags.okHttpAutomaticRetry());
|
||||
}
|
||||
|
||||
@@ -370,16 +376,25 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations() {
|
||||
return provideClientZkOperations().getReceiptOperations();
|
||||
public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration) {
|
||||
return provideClientZkOperations(signalServiceConfiguration).getReceiptOperations();
|
||||
}
|
||||
|
||||
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalWebSocketHealthMonitor healthMonitor) {
|
||||
@Override
|
||||
public @NonNull KeyBackupService provideKeyBackupService(@NonNull SignalServiceAccountManager signalServiceAccountManager, @NonNull KeyStore keyStore, @NonNull KbsEnclave enclave) {
|
||||
return signalServiceAccountManager.getKeyBackupService(keyStore,
|
||||
enclave.getEnclaveName(),
|
||||
Hex.fromStringOrThrow(enclave.getServiceId()),
|
||||
enclave.getMrEnclave(),
|
||||
10);
|
||||
}
|
||||
|
||||
private @NonNull WebSocketFactory provideWebSocketFactory(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull SignalWebSocketHealthMonitor healthMonitor) {
|
||||
return new WebSocketFactory() {
|
||||
@Override
|
||||
public WebSocketConnection createWebSocket() {
|
||||
return new WebSocketConnection("normal",
|
||||
provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
signalServiceConfiguration,
|
||||
Optional.of(new DynamicCredentialsProvider()),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
healthMonitor);
|
||||
@@ -388,7 +403,7 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
@Override
|
||||
public WebSocketConnection createUnidentifiedWebSocket() {
|
||||
return new WebSocketConnection("unidentified",
|
||||
provideSignalServiceNetworkAccess().getConfiguration(),
|
||||
signalServiceConfiguration,
|
||||
Optional.empty(),
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
healthMonitor);
|
||||
@@ -396,7 +411,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||
};
|
||||
}
|
||||
|
||||
private static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
@VisibleForTesting
|
||||
static class DynamicCredentialsProvider implements CredentialsProvider {
|
||||
|
||||
@Override
|
||||
public ACI getAci() {
|
||||
|
||||
@@ -646,7 +646,7 @@ final class GroupManagerV2 {
|
||||
|
||||
for (int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
return commitChange(authServiceId, change, allowWhenBlocked, sendToMembers);
|
||||
return commitChange(change, allowWhenBlocked, sendToMembers);
|
||||
} catch (GroupPatchNotAcceptedException e) {
|
||||
if (change.getAddMembersCount() > 0 && !refetchedAddMemberCredentials) {
|
||||
refetchedAddMemberCredentials = true;
|
||||
@@ -724,7 +724,7 @@ final class GroupManagerV2 {
|
||||
return change;
|
||||
}
|
||||
|
||||
private GroupManager.GroupActionResult commitChange(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
private GroupManager.GroupActionResult commitChange(@NonNull GroupChange.Actions.Builder change, boolean allowWhenBlocked, boolean sendToMembers)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
final GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
|
||||
@@ -741,7 +741,7 @@ final class GroupManagerV2 {
|
||||
|
||||
previousGroupState = v2GroupProperties.getDecryptedGroup();
|
||||
|
||||
GroupChange signedGroupChange = commitToServer(authServiceId, changeActions);
|
||||
GroupChange signedGroupChange = commitToServer(changeActions);
|
||||
try {
|
||||
//noinspection OptionalGetWithoutIsPresent
|
||||
decryptedChange = groupOperations.decryptChange(signedGroupChange, false).get();
|
||||
@@ -761,7 +761,7 @@ final class GroupManagerV2 {
|
||||
return new GroupManager.GroupActionResult(recipientAndThread.groupRecipient, recipientAndThread.threadId, newMembersCount, newPendingMembers);
|
||||
}
|
||||
|
||||
private @NonNull GroupChange commitToServer(@NonNull ServiceId authServiceId, @NonNull GroupChange.Actions change)
|
||||
private @NonNull GroupChange commitToServer(@NonNull GroupChange.Actions change)
|
||||
throws GroupNotAMemberException, GroupChangeFailedException, IOException, GroupInsufficientRightsException
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.KbsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository;
|
||||
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@@ -86,6 +87,7 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
String registrationLockV1 = null;
|
||||
String registrationLockV2 = null;
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId();
|
||||
|
||||
if (kbsValues.isV2RegistrationLockEnabled()) {
|
||||
registrationLockV2 = kbsValues.getRegistrationLockToken();
|
||||
@@ -115,12 +117,17 @@ public class RefreshAttributesJob extends BaseJob {
|
||||
"\n UUID? " + capabilities.isUuid());
|
||||
|
||||
SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
signalAccountManager.setAccountAttributes(null, registrationId, fetchesMessages,
|
||||
registrationLockV1, registrationLockV2,
|
||||
unidentifiedAccessKey, universalUnidentifiedAccess,
|
||||
signalAccountManager.setAccountAttributes(null,
|
||||
registrationId,
|
||||
fetchesMessages,
|
||||
registrationLockV1,
|
||||
registrationLockV2,
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
capabilities,
|
||||
phoneNumberDiscoverable,
|
||||
encryptedDeviceName);
|
||||
encryptedDeviceName,
|
||||
pniRegistrationId);
|
||||
|
||||
hasRefreshedThisAppCycle = true;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
private const val KEY_FCM_TOKEN_LAST_SET_TIME = "account.fcm_token_last_set_time"
|
||||
private const val KEY_DEVICE_NAME = "account.device_name"
|
||||
private const val KEY_DEVICE_ID = "account.device_id"
|
||||
private const val KEY_PNI_REGISTRATION_ID = "account.pni_registration_id"
|
||||
|
||||
private const val KEY_ACI_IDENTITY_PUBLIC_KEY = "account.aci_identity_public_key"
|
||||
private const val KEY_ACI_IDENTITY_PRIVATE_KEY = "account.aci_identity_private_key"
|
||||
@@ -135,6 +136,8 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
/** A randomly-generated value that represents this registration instance. Helps the server know if you reinstalled. */
|
||||
var registrationId: Int by integerValue(KEY_REGISTRATION_ID, 0)
|
||||
|
||||
var pniRegistrationId: Int by integerValue(KEY_PNI_REGISTRATION_ID, 0)
|
||||
|
||||
/** The identity key pair for the ACI identity. */
|
||||
val aciIdentityKey: IdentityKeyPair
|
||||
get() {
|
||||
@@ -202,7 +205,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
}
|
||||
|
||||
/** When acting as a linked device, this method lets you store the identity keys sent from the primary device */
|
||||
fun setIdentityKeysFromPrimaryDevice(aciKeys: IdentityKeyPair) {
|
||||
fun setAciIdentityKeysFromPrimaryDevice(aciKeys: IdentityKeyPair) {
|
||||
synchronized(this) {
|
||||
require(isLinkedDevice) { "Must be a linked device!" }
|
||||
store
|
||||
@@ -213,6 +216,19 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
|
||||
}
|
||||
}
|
||||
|
||||
/** Set an identity key pair for the PNI identity via change number. */
|
||||
fun setPniIdentityKeyAfterChangeNumber(key: IdentityKeyPair) {
|
||||
synchronized(this) {
|
||||
Log.i(TAG, "Setting a new PNI identity key pair.")
|
||||
|
||||
store
|
||||
.beginWrite()
|
||||
.putBlob(KEY_PNI_IDENTITY_PUBLIC_KEY, key.publicKey.serialize())
|
||||
.putBlob(KEY_PNI_IDENTITY_PRIVATE_KEY, key.privateKey.serialize())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/** Only to be used when restoring an identity public key from an old backup */
|
||||
fun restoreLegacyIdentityPublicKeyFromBackup(base64: String) {
|
||||
Log.w(TAG, "Restoring legacy identity public key from backup.")
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.keyvalue;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -17,6 +19,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
private static final String OLD_DEVICE_TRANSFER_LOCKED = "misc.old_device.transfer.locked";
|
||||
private static final String HAS_EVER_HAD_AN_AVATAR = "misc.has.ever.had.an.avatar";
|
||||
private static final String CHANGE_NUMBER_LOCK = "misc.change_number.lock";
|
||||
private static final String PENDING_CHANGE_NUMBER_METADATA = "misc.pending_change_number.metadata";
|
||||
private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time";
|
||||
private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable";
|
||||
private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time";
|
||||
@@ -117,6 +120,20 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||
putBoolean(CHANGE_NUMBER_LOCK, false);
|
||||
}
|
||||
|
||||
public @Nullable PendingChangeNumberMetadata getPendingChangeNumberMetadata() {
|
||||
return getObject(PENDING_CHANGE_NUMBER_METADATA, null, PendingChangeNumberMetadataSerializer.INSTANCE);
|
||||
}
|
||||
|
||||
/** Store pending new PNI data to be applied after successful change number */
|
||||
public void setPendingChangeNumberMetadata(@NonNull PendingChangeNumberMetadata metadata) {
|
||||
putObject(PENDING_CHANGE_NUMBER_METADATA, metadata, PendingChangeNumberMetadataSerializer.INSTANCE);
|
||||
}
|
||||
|
||||
/** Clear pending new PNI data after confirmed successful or failed change number */
|
||||
public void clearPendingChangeNumberMetadata() {
|
||||
remove(PENDING_CHANGE_NUMBER_METADATA);
|
||||
}
|
||||
|
||||
public long getLastCensorshipServiceReachabilityCheckTime() {
|
||||
return getLong(CENSORSHIP_LAST_CHECK_TIME, 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import org.signal.core.util.ByteSerializer
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
|
||||
/**
|
||||
* Serialize [PendingChangeNumberMetadata]
|
||||
*/
|
||||
object PendingChangeNumberMetadataSerializer : ByteSerializer<PendingChangeNumberMetadata> {
|
||||
override fun serialize(data: PendingChangeNumberMetadata): ByteArray = data.toByteArray()
|
||||
override fun deserialize(data: ByteArray): PendingChangeNumberMetadata = PendingChangeNumberMetadata.parseFrom(data)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.core.util.ByteSerializer;
|
||||
import org.signal.core.util.StringSerializer;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SignalStoreList;
|
||||
|
||||
@@ -51,6 +53,15 @@ abstract class SignalStoreValues {
|
||||
return store.getBlob(key, defaultValue);
|
||||
}
|
||||
|
||||
<T> T getObject(@NonNull String key, @Nullable T defaultValue, @NonNull ByteSerializer<T> serializer) {
|
||||
byte[] blob = store.getBlob(key, null);
|
||||
if (blob == null) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return serializer.deserialize(blob);
|
||||
}
|
||||
}
|
||||
|
||||
<T> List<T> getList(@NonNull String key, @NonNull StringSerializer<T> serializer) {
|
||||
byte[] blob = getBlob(key, null);
|
||||
if (blob == null) {
|
||||
@@ -94,6 +105,10 @@ abstract class SignalStoreValues {
|
||||
store.beginWrite().putString(key, value).apply();
|
||||
}
|
||||
|
||||
<T> void putObject(@NonNull String key, T value, @NonNull ByteSerializer<T> serializer) {
|
||||
putBlob(key, serializer.serialize(value));
|
||||
}
|
||||
|
||||
<T> void putList(@NonNull String key, @NonNull List<T> values, @NonNull StringSerializer<T> serializer) {
|
||||
putBlob(key, SignalStoreList.newBuilder()
|
||||
.addAllContents(values.stream()
|
||||
|
||||
@@ -105,9 +105,10 @@ public class ApplicationMigrations {
|
||||
static final int MY_STORY_PRIVACY_MODE = 61;
|
||||
static final int REFRESH_EXPIRING_CREDENTIAL = 62;
|
||||
static final int EMOJI_SEARCH_INDEX_10 = 63;
|
||||
static final int REFRESH_PNI_REGISTRATION_ID = 64;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 63;
|
||||
public static final int CURRENT_VERSION = 64;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -461,6 +462,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.EMOJI_SEARCH_INDEX_10, new EmojiDownloadMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.REFRESH_PNI_REGISTRATION_ID) {
|
||||
jobs.put(Version.REFRESH_PNI_REGISTRATION_ID, new AttributesMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ public class PhoneNumberFormatter {
|
||||
return StringUtil.isolateBidi(phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL));
|
||||
}
|
||||
} catch (NumberParseException e) {
|
||||
Log.w(TAG, "Failed to format number.");
|
||||
Log.w(TAG, "Failed to format number: " + e.toString());
|
||||
return StringUtil.isolateBidi(e164);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ public class PhoneNumberFormatter {
|
||||
Phonenumber.PhoneNumber parsedNumber = phoneNumberUtil.parse(processedNumber, localCountryCode);
|
||||
return phoneNumberUtil.format(parsedNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
|
||||
} catch (NumberParseException e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, e.toString());
|
||||
if (bareNumber.charAt(0) == '+') {
|
||||
return bareNumber;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
* Using provided or already stored authorization, provides various get token data from KBS
|
||||
* and generate {@link KbsPinData}.
|
||||
*/
|
||||
public final class KbsRepository {
|
||||
public class KbsRepository {
|
||||
|
||||
private static final String TAG = Log.tag(KbsRepository.class);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public class AccountManagerFactory {
|
||||
});
|
||||
}
|
||||
|
||||
return new SignalServiceAccountManager(new SignalServiceNetworkAccess(context).getConfiguration(number),
|
||||
return new SignalServiceAccountManager(ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
|
||||
null,
|
||||
null,
|
||||
number,
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.Optional
|
||||
* Provides a [SignalServiceConfiguration] to be used with our service layer.
|
||||
* If you're looking for a place to start, look at [getConfiguration].
|
||||
*/
|
||||
class SignalServiceNetworkAccess(context: Context) {
|
||||
open class SignalServiceNetworkAccess(context: Context) {
|
||||
companion object {
|
||||
private val TAG = Log.tag(SignalServiceNetworkAccess::class.java)
|
||||
|
||||
@@ -227,11 +227,11 @@ class SignalServiceNetworkAccess(context: Context) {
|
||||
zkGroupServerPublicParams
|
||||
)
|
||||
|
||||
fun getConfiguration(): SignalServiceConfiguration {
|
||||
open fun getConfiguration(): SignalServiceConfiguration {
|
||||
return getConfiguration(SignalStore.account().e164)
|
||||
}
|
||||
|
||||
fun getConfiguration(localNumber: String?): SignalServiceConfiguration {
|
||||
open fun getConfiguration(localNumber: String?): SignalServiceConfiguration {
|
||||
if (localNumber == null || SignalStore.proxy().isProxyEnabled) {
|
||||
return uncensoredConfiguration
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ data class RegistrationData(
|
||||
val password: String,
|
||||
val registrationId: Int,
|
||||
val profileKey: ProfileKey,
|
||||
val fcmToken: String?
|
||||
val fcmToken: String?,
|
||||
val pniRegistrationId: Int
|
||||
) {
|
||||
val isFcm: Boolean = fcmToken != null
|
||||
val isNotFcm: Boolean = fcmToken == null
|
||||
|
||||
@@ -73,6 +73,15 @@ public final class RegistrationRepository {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public int getPniRegistrationId() {
|
||||
int pniRegistrationId = SignalStore.account().getPniRegistrationId();
|
||||
if (pniRegistrationId == 0) {
|
||||
pniRegistrationId = KeyHelper.generateRegistrationId(false);
|
||||
SignalStore.account().setPniRegistrationId(pniRegistrationId);
|
||||
}
|
||||
return pniRegistrationId;
|
||||
}
|
||||
|
||||
public @NonNull ProfileKey getProfileKey(@NonNull String e164) {
|
||||
ProfileKey profileKey = findExistingProfileKey(e164);
|
||||
|
||||
|
||||
@@ -70,7 +70,8 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable,
|
||||
registrationData.pniRegistrationId
|
||||
)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
@@ -99,7 +100,8 @@ class VerifyAccountRepository(private val context: Application) {
|
||||
unidentifiedAccessKey,
|
||||
universalUnidentifiedAccess,
|
||||
AppCapabilities.getCapabilities(true),
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable,
|
||||
registrationData.pniRegistrationId
|
||||
)
|
||||
VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
|
||||
} catch (e: KeyBackupSystemWrongPinException) {
|
||||
|
||||
@@ -130,7 +130,8 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
|
||||
getRegistrationSecret(),
|
||||
registrationRepository.getRegistrationId(),
|
||||
registrationRepository.getProfileKey(getNumber().getE164Number()),
|
||||
getFcmToken());
|
||||
getFcmToken(),
|
||||
registrationRepository.getPniRegistrationId());
|
||||
}
|
||||
|
||||
public static final class Factory extends AbstractSavedStateViewModelFactory {
|
||||
|
||||
Reference in New Issue
Block a user