diff --git a/app/build.gradle b/app/build.gradle index cfe20a446..d0a8f1169 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -231,7 +231,7 @@ android { } } - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner" testInstrumentationRunnerArguments clearPackageData: 'true' } @@ -544,6 +544,9 @@ dependencies { androidTestImplementation testLibs.androidx.test.core androidTestImplementation testLibs.androidx.test.core.ktx androidTestImplementation testLibs.androidx.test.ext.junit.ktx + androidTestImplementation testLibs.mockito.android + androidTestImplementation testLibs.mockito.kotlin + androidTestImplementation testLibs.square.okhttp.mockserver testImplementation testLibs.espresso.core diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/SignalInstrumentationApplicationContext.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/SignalInstrumentationApplicationContext.kt new file mode 100644 index 000000000..0fdd1d28e --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/SignalInstrumentationApplicationContext.kt @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider +import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider + +/** + * Application context for running instrumentation tests (aka androidTests). + */ +class SignalInstrumentationApplicationContext : ApplicationContext() { + override fun initializeAppDependencies() { + val default = ApplicationDependencyProvider(this) + ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default)) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt new file mode 100644 index 000000000..a02f8220e --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModelTest.kt @@ -0,0 +1,272 @@ +package org.thoughtcrime.securesms.components.settings.app.changenumber + +import androidx.lifecycle.SavedStateHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import okhttp3.mockwebserver.MockResponse +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.signal.core.util.ThreadUtil +import org.signal.libsignal.protocol.state.SignedPreKeyRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.KbsRepository +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.registration.VerifyAccountRepository +import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor +import org.thoughtcrime.securesms.testing.Get +import org.thoughtcrime.securesms.testing.MockProvider +import org.thoughtcrime.securesms.testing.Put +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.testing.assertIs +import org.thoughtcrime.securesms.testing.assertIsNot +import org.thoughtcrime.securesms.testing.assertIsNotNull +import org.thoughtcrime.securesms.testing.assertIsNull +import org.thoughtcrime.securesms.testing.assertIsSize +import org.thoughtcrime.securesms.testing.connectionFailure +import org.thoughtcrime.securesms.testing.failure +import org.thoughtcrime.securesms.testing.parsedRequestBody +import org.thoughtcrime.securesms.testing.success +import org.thoughtcrime.securesms.testing.timeout +import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.internal.push.PreKeyState +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class ChangeNumberViewModelTest { + + @get:Rule + val harness = SignalActivityRule() + + private lateinit var viewModel: ChangeNumberViewModel + private lateinit var kbsRepository: KbsRepository + + @Before + fun setUp() { + ApplicationDependencies.getSignalServiceAccountManager().setSoTimeoutMillis(1000) + kbsRepository = mock() + ThreadUtil.runOnMainSync { + viewModel = ChangeNumberViewModel( + localNumber = harness.self.requireE164(), + changeNumberRepository = ChangeNumberRepository(), + savedState = SavedStateHandle(), + password = SignalStore.account().servicePassword!!, + verifyAccountRepository = VerifyAccountRepository(harness.application), + kbsRepository = kbsRepository + ) + + viewModel.setNewCountry(1) + viewModel.setNewNationalNumber("5555550102") + } + } + + @After + fun tearDown() { + InstrumentationApplicationDependencyProvider.clearHandlers() + } + + @Test + fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() { + // GIVEN + val aci = Recipient.self().requireServiceId() + val newPni = ServiceId.from(UUID.randomUUID()) + lateinit var changeNumberRequest: ChangePhoneNumberRequest + lateinit var setPreKeysRequest: PreKeyState + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, + Put("/v1/accounts/number") { r -> + changeNumberRequest = r.parsedRequestBody() + MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) + }, + Put("/v2/keys") { r -> + setPreKeysRequest = r.parsedRequestBody() + MockResponse().success() + }, + Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } + ) + + // WHEN + viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow + + // THEN + assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) + } + + /** + * If we encounter a server error, this means the server ack our request and rejected it. In this + * case we know the change *did not* take on the server and can reset to a clean state. + */ + @Test + fun testChangeNumber_givenServerFailedApiCall() { + // GIVEN + val oldPni = Recipient.self().requirePni() + val oldE164 = Recipient.self().requireE164() + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, + Put("/v1/accounts/number") { MockResponse().failure(500) }, + ) + + // WHEN + val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() + + // THEN + processor.isServerSentError() assertIs true + Recipient.self().requireE164() assertIs oldE164 + Recipient.self().requirePni() assertIs oldPni + SignalStore.misc().pendingChangeNumberMetadata.assertIsNull() + } + + /** + * If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change + * number on the server side. We have to do a whoami call to query the server for our details and then + * respond accordingly. + * + * In this case, the whoami is our old details, so we can know the change *did not* take on the server + * and can reset to a clean state. + */ + @Test + fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() { + // GIVEN + val aci = Recipient.self().requireServiceId() + val oldPni = Recipient.self().requirePni() + val oldE164 = Recipient.self().requireE164() + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, + Put("/v1/accounts/number") { MockResponse().connectionFailure() }, + Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) } + ) + + // WHEN + val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() + + // THEN + processor.isServerSentError() assertIs false + Recipient.self().requireE164() assertIs oldE164 + Recipient.self().requirePni() assertIs oldPni + SignalStore.misc().isChangeNumberLocked assertIs false + SignalStore.misc().pendingChangeNumberMetadata.assertIsNull() + } + + /** + * If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change + * number on the server side. We have to do a whoami call to query the server for our details and then + * respond accordingly. + * + * In this case, the whoami is our new details, so we can know the change *did* take on the server + * and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock + * and apply the pending state after confirming the change on the server. + */ + @Test + fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() { + // GIVEN + val aci = Recipient.self().requireServiceId() + val oldPni = Recipient.self().requirePni() + val oldE164 = Recipient.self().requireE164() + val newPni = ServiceId.from(UUID.randomUUID()) + + lateinit var changeNumberRequest: ChangePhoneNumberRequest + lateinit var setPreKeysRequest: PreKeyState + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, + Put("/v1/accounts/number") { r -> + changeNumberRequest = r.parsedRequestBody() + MockResponse().timeout() + }, + Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) }, + Put("/v2/keys") { r -> + setPreKeysRequest = r.parsedRequestBody() + MockResponse().success() + }, + Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } + ) + + // WHEN + val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet() + + // THEN + processor.isServerSentError() assertIs false + Recipient.self().requireE164() assertIs oldE164 + Recipient.self().requirePni() assertIs oldPni + SignalStore.misc().isChangeNumberLocked assertIs true + SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull() + + // WHEN AGAIN Processing lock + val scenario = harness.launchActivity() + scenario.onActivity {} + ThreadUtil.sleep(500) + + // THEN AGAIN + assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) + } + + @Test + fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() { + // GIVEN + val aci = Recipient.self().requireServiceId() + val newPni = ServiceId.from(UUID.randomUUID()) + + lateinit var changeNumberRequest: ChangePhoneNumberRequest + lateinit var setPreKeysRequest: PreKeyState + + MockProvider.mockGetRegistrationLockStringFlow(kbsRepository) + + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( + Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) }, + Put("/v1/accounts/number") { r -> + changeNumberRequest = r.parsedRequestBody() + if (changeNumberRequest.registrationLock.isNullOrEmpty()) { + MockResponse().failure(423, MockProvider.lockedFailure) + } else { + MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni)) + } + }, + Put("/v2/keys") { r -> + setPreKeysRequest = r.parsedRequestBody() + MockResponse().success() + }, + Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) } + ) + + // WHEN + viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor -> + processor.registrationLock() assertIs true + Recipient.self().requirePni() assertIsNot newPni + SignalStore.misc().pendingChangeNumberMetadata.assertIsNull() + } + + viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow + + // THEN + assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) + } + + private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) { + val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni() + val pniMetadataStore = SignalStore.account().pniPreKeys + + Recipient.self().requireE164() assertIs "+15555550102" + Recipient.self().requirePni() assertIs newPni + + SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!! + SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey + pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId + + val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId) + activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey + activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature + + setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey + setPreKeysRequest.preKeys assertIsSize 100 + + SignalStore.misc().pendingChangeNumberMetadata.assertIsNull() + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt new file mode 100644 index 000000000..bc1dd5f12 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt @@ -0,0 +1,109 @@ +package org.thoughtcrime.securesms.dependencies + +import android.app.Application +import okhttp3.ConnectionSpec +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.KbsEnclave +import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess +import org.thoughtcrime.securesms.push.SignalServiceTrustStore +import org.thoughtcrime.securesms.testing.Verb +import org.thoughtcrime.securesms.testing.runSync +import org.thoughtcrime.securesms.util.Base64 +import org.whispersystems.signalservice.api.KeyBackupService +import org.whispersystems.signalservice.api.SignalServiceAccountManager +import org.whispersystems.signalservice.api.push.TrustStore +import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl +import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl +import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration +import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl +import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl +import java.security.KeyStore +import java.util.Optional + +/** + * Dependency provider used for instrumentation tests (aka androidTests). + * + * Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess] and + * [KeyBackupService]. + */ +class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default { + + private val serviceTrustStore: TrustStore + private val uncensoredConfiguration: SignalServiceConfiguration + private val serviceNetworkAccessMock: SignalServiceNetworkAccess + private val keyBackupService: KeyBackupService + + init { + runSync { + webServer = MockWebServer() + baseUrl = webServer.url("").toString() + } + + webServer.setDispatcher(object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + val handler = handlers.firstOrNull { + request.method == it.verb && request.path.startsWith("/${it.path}") + } + return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500) + } + }) + + serviceTrustStore = SignalServiceTrustStore(application) + uncensoredConfiguration = SignalServiceConfiguration( + arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + mapOf( + 0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + 2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)) + ), + arrayOf(SignalContactDiscoveryUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)), + emptyList(), + Optional.of(SignalServiceNetworkAccess.DNS), + Optional.empty(), + Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS) + ) + + serviceNetworkAccessMock = mock { + on { getConfiguration() } doReturn uncensoredConfiguration + on { getConfiguration(any()) } doReturn uncensoredConfiguration + } + + keyBackupService = mock() + } + + override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess { + return serviceNetworkAccessMock + } + + override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService { + return keyBackupService + } + + companion object { + lateinit var webServer: MockWebServer + private set + lateinit var baseUrl: String + private set + + private val handlers: MutableList = mutableListOf() + + fun addMockWebRequestHandlers(vararg verbs: Verb) { + handlers.addAll(verbs) + } + + fun clearHandlers() { + handlers.clear() + } + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MockProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MockProvider.kt new file mode 100644 index 000000000..825b84838 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/MockProvider.kt @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.testing + +import io.reactivex.rxjava3.core.Single +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.signal.core.util.Hex +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.pin.KbsRepository +import org.thoughtcrime.securesms.pin.TokenData +import org.thoughtcrime.securesms.test.BuildConfig +import org.whispersystems.signalservice.api.KbsPinData +import org.whispersystems.signalservice.api.KeyBackupService +import org.whispersystems.signalservice.api.kbs.HashedPin +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse +import org.whispersystems.signalservice.internal.push.AuthCredentials +import org.whispersystems.signalservice.internal.push.DeviceInfoList +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import org.whispersystems.signalservice.internal.push.SenderCertificate +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse +import org.whispersystems.signalservice.internal.push.WhoAmIResponse +import java.security.SecureRandom + +/** + * Warehouse of reusable test data and mock configurations. + */ +object MockProvider { + + val senderCertificate = SenderCertificate().apply { certificate = ByteArray(0) } + + val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply { + backupCredentials = AuthCredentials.create("username", "password") + } + + val primaryOnlyDeviceList = DeviceInfoList().apply { + devices = listOf( + DeviceInfo().apply { + id = 1 + } + ) + } + + fun createVerifyAccountResponse(aci: ServiceId, newPni: ServiceId): VerifyAccountResponse { + return VerifyAccountResponse().apply { + uuid = aci.toString() + pni = newPni.toString() + storageCapable = false + } + } + + fun createWhoAmIResponse(aci: ServiceId, pni: ServiceId, e164: String): WhoAmIResponse { + return WhoAmIResponse().apply { + this.uuid = aci.toString() + this.pni = pni.toString() + this.number = e164 + } + } + + fun mockGetRegistrationLockStringFlow(kbsRepository: KbsRepository) { + val tokenData: TokenData = mock { + on { enclave } doReturn BuildConfig.KBS_ENCLAVE + on { basicAuth } doReturn "basicAuth" + on { triesRemaining } doReturn 10 + on { tokenResponse } doReturn TokenResponse() + } + + kbsRepository.stub { + on { getToken(any() as String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, "")) + } + + val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession { + override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8") + override fun restorePin(hashedPin: HashedPin?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null) + } + + val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE) + kbsService.stub { + on { newRegistrationSession(any(), any()) } doReturn session + } + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/ResponseMocking.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/ResponseMocking.kt new file mode 100644 index 000000000..20268a8be --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/ResponseMocking.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.testing + +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest +import okhttp3.mockwebserver.SocketPolicy +import org.thoughtcrime.securesms.util.JsonUtils +import java.util.concurrent.TimeUnit + +typealias ResponseFactory = (request: RecordedRequest) -> MockResponse + +/** + * Represent an HTTP verb for mocking web requests. + */ +sealed class Verb(val verb: String, val path: String, val responseFactory: ResponseFactory) + +class Get(path: String, responseFactory: ResponseFactory) : Verb("GET", path, responseFactory) + +class Put(path: String, responseFactory: ResponseFactory) : Verb("PUT", path, responseFactory) + +fun MockResponse.success(response: Any? = null): MockResponse { + return setResponseCode(200).apply { + if (response != null) { + setBody(JsonUtils.toJson(response)) + } + } +} + +fun MockResponse.failure(code: Int, response: Any? = null): MockResponse { + return setResponseCode(code).apply { + if (response != null) { + setBody(JsonUtils.toJson(response)) + } + } +} + +fun MockResponse.connectionFailure(): MockResponse { + return setSocketPolicy(SocketPolicy.DISCONNECT_AT_START) +} + +fun MockResponse.timeout(): MockResponse { + return setHeadersDelay(1, TimeUnit.DAYS) + .setBodyDelay(1, TimeUnit.DAYS) +} + +inline fun RecordedRequest.parsedRequestBody(): T { + val bodyString = String(body.readByteArray()) + return JsonUtils.fromJson(bodyString, T::class.java) +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt index d2f580568..2022359f7 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalActivityRule.kt @@ -8,6 +8,7 @@ import android.content.SharedPreferences import android.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.platform.app.InstrumentationRegistry +import okhttp3.mockwebserver.MockResponse import org.junit.rules.ExternalResource import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.SignalProtocolAddress @@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil import org.thoughtcrime.securesms.database.IdentityDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor import org.thoughtcrime.securesms.profiles.ProfileName @@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import java.lang.IllegalArgumentException import java.util.UUID @@ -54,6 +58,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() context = InstrumentationRegistry.getInstrumentation().targetContext self = setupSelf() others = setupOthers() + + InstrumentationApplicationDependencyProvider.clearHandlers() } private fun setupSelf(): Recipient { @@ -67,18 +73,22 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() val registrationRepository = RegistrationRepository(application) - registrationRepository.registerAccountWithoutRegistrationLock( + InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() }) + val response: ServiceResponse = registrationRepository.registerAccountWithoutRegistrationLock( RegistrationData( code = "123123", - e164 = "+15554045550101", + e164 = "+15555550101", password = Util.getSecret(18), registrationId = registrationRepository.registrationId, - profileKey = registrationRepository.getProfileKey("+15554045550101"), - fcmToken = null + profileKey = registrationRepository.getProfileKey("+15555550101"), + fcmToken = null, + pniRegistrationId = registrationRepository.pniRegistrationId ), VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false) ).blockingGet() + ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow + SignalStore.kbsValues().optOut() RegistrationUtil.maybeMarkRegistrationComplete(application) SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson")) @@ -107,7 +117,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() return others } - inline fun launchActivity(initIntent: Intent.() -> Unit): ActivityScenario { + inline fun launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario { return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent)) } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalTestRunner.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalTestRunner.kt new file mode 100644 index 000000000..0c879c228 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/SignalTestRunner.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.testing + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext + +/** + * Custom runner that replaces application with [SignalInstrumentationApplicationContext]. + */ +@Suppress("unused") +class SignalTestRunner : AndroidJUnitRunner() { + override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { + return super.newApplication(cl, SignalInstrumentationApplicationContext::class.java.name, context) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestUtils.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestUtils.kt new file mode 100644 index 000000000..0604ec82a --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/TestUtils.kt @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.testing + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasSize +import org.hamcrest.Matchers.`is` +import org.hamcrest.Matchers.not +import org.hamcrest.Matchers.notNullValue +import org.hamcrest.Matchers.nullValue +import java.util.concurrent.CountDownLatch + +/** + * Run the given [runnable] on a new thread and wait for it to finish. + */ +fun runSync(runnable: () -> Unit) { + val lock = CountDownLatch(1) + Thread { + try { + runnable.invoke() + } finally { + lock.countDown() + } + }.start() + lock.await() +} + +/* Various kotlin-ifications of hamcrest matchers */ + +fun T.assertIsNull() { + assertThat(this, nullValue()) +} + +fun T.assertIsNotNull() { + assertThat(this, notNullValue()) +} + +infix fun T.assertIs(expected: T) { + assertThat(this, `is`(expected)) +} + +infix fun T.assertIsNot(expected: T) { + assertThat(this, not(`is`(expected))) +} + +infix fun > T.assertIsSize(expected: Int) { + assertThat(this, hasSize(expected)) +} diff --git a/app/src/instrumentation/AndroidManifest.xml b/app/src/instrumentation/AndroidManifest.xml new file mode 100644 index 000000000..6c9103481 --- /dev/null +++ b/app/src/instrumentation/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index ef4783386..c2faaf222 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt index e3cb9660c..c97694083 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberLockActivity.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index e1150321a..d4884b78d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -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> { - 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 = accountManager.changeNumber(code, newE164, registrationLock) + SignalStore.misc().setPendingChangeNumberMetadata(metadata) + + val response: ServiceResponse = 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 = accountManager.getDevices() + + val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair() + val deviceMessages = mutableListOf() + val devicePniSignedPreKeys = mutableMapOf() + val pniRegistrationIds = mutableMapOf() + 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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt index c9951f21b..72cad36b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberVerifyFragment.kt @@ -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() } - ) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt index c80495afd..8ce9a9bc7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberViewModel.kt @@ -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 { return super.verifyCodeWithoutRegistrationLock(code) .doOnSubscribe { SignalStore.misc().lockChangeNumber() } @@ -122,6 +127,7 @@ class ChangeNumberViewModel( private fun attemptToUnlockChangeNumber(processor: T): Single { 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), diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 85448a8cc..cf743eef3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtoExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtoExtensions.kt index d7d2d3de4..c34df7783 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtoExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DatabaseProtoExtensions.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 548d93b33..1b4c3405a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -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); } } 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 b5cd86b70..e8cdcaf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 980a28733..82347caa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 7264d314e..2fdd4c047 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt index c753ef0d6..267acba21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt @@ -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.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index f5f9e30cf..4afd45582 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PendingChangeNumberMetadataSerializer.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PendingChangeNumberMetadataSerializer.kt new file mode 100644 index 000000000..4a3b4c841 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PendingChangeNumberMetadataSerializer.kt @@ -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 { + override fun serialize(data: PendingChangeNumberMetadata): ByteArray = data.toByteArray() + override fun deserialize(data: ByteArray): PendingChangeNumberMetadata = PendingChangeNumberMetadata.parseFrom(data) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValues.java index 39a6c759e..b182609e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStoreValues.java @@ -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 getObject(@NonNull String key, @Nullable T defaultValue, @NonNull ByteSerializer serializer) { + byte[] blob = store.getBlob(key, null); + if (blob == null) { + return defaultValue; + } else { + return serializer.deserialize(blob); + } + } + List getList(@NonNull String key, @NonNull StringSerializer serializer) { byte[] blob = getBlob(key, null); if (blob == null) { @@ -94,6 +105,10 @@ abstract class SignalStoreValues { store.beginWrite().putString(key, value).apply(); } + void putObject(@NonNull String key, T value, @NonNull ByteSerializer serializer) { + putBlob(key, serializer.serialize(value)); + } + void putList(@NonNull String key, @NonNull List values, @NonNull StringSerializer serializer) { putBlob(key, SignalStoreList.newBuilder() .addAllContents(values.stream() diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 3c09afbe0..e31701891 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java b/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java index b71151384..364f6754f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/phonenumbers/PhoneNumberFormatter.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java index 1fa6e836d..d85b6efe4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java @@ -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); 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 6184fc8d5..ea7c44a42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/AccountManagerFactory.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt index ecb0330e9..127c4c383 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt index fc7e80405..8ae01102d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt @@ -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 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 f587905f0..1c0d7570e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt index e1fdaaec7..5933e0199 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index 05ff18055..9131d5f54 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -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 { diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 716010f6e..70ed54c67 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -238,4 +238,11 @@ message GiftBadge { message SignalStoreList { repeated string contents = 1; -} \ No newline at end of file +} + +message PendingChangeNumberMetadata { + bytes previousPni = 1; + bytes pniIdentityKeyPair = 2; + int32 pniRegistrationId = 3; + int32 pniSignedPreKeyId = 4; +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java index 8b2802b16..ec282fe9a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.java @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; import org.signal.core.util.concurrent.DeadlockDetector; import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; +import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.components.TypingStatusRepository; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl; @@ -32,6 +33,7 @@ import org.thoughtcrime.securesms.util.FrameRateTracker; 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; @@ -40,27 +42,31 @@ import org.whispersystems.signalservice.api.SignalWebSocket; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; + +import java.security.KeyStore; import static org.mockito.Mockito.mock; +@SuppressWarnings("ConstantConditions") public class MockApplicationDependencyProvider implements ApplicationDependencies.Provider { @Override - public @NonNull GroupsV2Operations provideGroupsV2Operations() { + public @NonNull GroupsV2Operations provideGroupsV2Operations(@NonNull SignalServiceConfiguration signalServiceConfiguration) { return null; } @Override - public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() { + public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) { return null; } @Override - public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore) { + public @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(@NonNull SignalWebSocket signalWebSocket, @NonNull SignalServiceDataStore protocolStore, @NonNull SignalServiceConfiguration signalServiceConfiguration) { return null; } @Override - public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver() { + public @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(@NonNull SignalServiceConfiguration signalServiceConfiguration) { return null; } @@ -180,7 +186,7 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie } @Override - public @NonNull SignalWebSocket provideSignalWebSocket() { + public @NonNull SignalWebSocket provideSignalWebSocket(@NonNull SignalServiceConfiguration signalServiceConfiguration) { return null; } @@ -205,7 +211,7 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie } @Override - public @NonNull DonationsService provideDonationsService() { + public @NonNull DonationsService provideDonationsService(@NonNull SignalServiceConfiguration signalServiceConfiguration, @NonNull GroupsV2Operations groupsV2Operations) { return null; } @@ -220,7 +226,12 @@ public class MockApplicationDependencyProvider implements ApplicationDependencie } @Override - public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations() { + public @NonNull ClientZkReceiptOperations provideClientZkReceiptOperations(@NonNull SignalServiceConfiguration signalServiceConfiguration) { + return null; + } + + @Override + public @NonNull KeyBackupService provideKeyBackupService(@NonNull SignalServiceAccountManager signalServiceAccountManager, @NonNull KeyStore keyStore, @NonNull KbsEnclave enclave) { return null; } } diff --git a/core-util/src/main/java/org/signal/core/util/Serializer.kt b/core-util/src/main/java/org/signal/core/util/Serializer.kt index d8c5385ed..6ab30e9b2 100644 --- a/core-util/src/main/java/org/signal/core/util/Serializer.kt +++ b/core-util/src/main/java/org/signal/core/util/Serializer.kt @@ -11,3 +11,5 @@ interface Serializer { interface StringSerializer : Serializer interface LongSerializer : Serializer + +interface ByteSerializer : Serializer diff --git a/dependencies.gradle b/dependencies.gradle index 37755e7ca..a86b9b81c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -131,13 +131,15 @@ dependencyResolutionManagement { alias('androidx-test-ext-junit').to('androidx.test.ext:junit:1.1.1') alias('androidx-test-ext-junit-ktx').to('androidx.test.ext:junit-ktx:1.1.1') alias('espresso-core').to('androidx.test.espresso:espresso-core:3.4.0') - alias('mockito-core').to('org.mockito:mockito-inline:4.4.0') + alias('mockito-core').to('org.mockito:mockito-inline:4.6.1') alias('mockito-kotlin').to('org.mockito.kotlin:mockito-kotlin:4.0.0') + alias('mockito-android').to('org.mockito:mockito-android:4.6.1') alias('robolectric-robolectric').to('org.robolectric', 'robolectric').versionRef('robolectric') alias('robolectric-shadows-multidex').to('org.robolectric', 'shadows-multidex').versionRef('robolectric') alias('bouncycastle-bcprov-jdk15on').to('org.bouncycastle:bcprov-jdk15on:1.70') alias('hamcrest-hamcrest').to('org.hamcrest:hamcrest:2.2') alias('assertj-core').to('org.assertj:assertj-core:3.11.1') + alias('square-okhttp-mockserver').to('com.squareup.okhttp3:mockwebserver:3.12.13') alias('conscrypt-openjdk-uber').to('org.conscrypt:conscrypt-openjdk-uber:2.0.0') } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 209158386..897d60454 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2795,6 +2795,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3103,6 +3108,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3740,6 +3750,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3756,6 +3771,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -3772,6 +3792,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -5229,6 +5254,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -5245,6 +5275,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + @@ -5253,6 +5288,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html + + + + + diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KbsPinData.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KbsPinData.java index c3da20c06..58df4334b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KbsPinData.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KbsPinData.java @@ -8,7 +8,8 @@ public final class KbsPinData { private final MasterKey masterKey; private final TokenResponse tokenResponse; - KbsPinData(MasterKey masterKey, TokenResponse tokenResponse) { + // Visible for testing + public KbsPinData(MasterKey masterKey, TokenResponse tokenResponse) { this.masterKey = masterKey; this.tokenResponse = tokenResponse; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java index d2145a915..f701c62a9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java @@ -26,7 +26,7 @@ import java.security.SecureRandom; import java.security.SignatureException; import java.util.Locale; -public final class KeyBackupService { +public class KeyBackupService { private static final String TAG = KeyBackupService.class.getSimpleName(); 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 ed80b236d..ea0f0b0f6 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.signal.libsignal.protocol.state.SignedPreKeyRecord; 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.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; @@ -106,6 +107,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; +import javax.annotation.Nonnull; + import io.reactivex.rxjava3.core.Single; import static org.whispersystems.signalservice.internal.push.ProvisioningProtos.ProvisionMessage; @@ -181,7 +184,7 @@ public class SignalServiceAccountManager { * V1 PINs are no longer used in favor of V2 PINs stored on KBS. * * You can remove a V1 PIN, but typically this is unnecessary, as setting a V2 PIN via - * {@link KeyBackupService.Session#enableRegistrationLock(MasterKey)}} will automatically clear the + * {@link KeyBackupService.PinChangeSession#enableRegistrationLock(MasterKey)}} will automatically clear the * V1 PIN on the service. */ public void removeRegistrationLockV1() throws IOException { @@ -280,7 +283,8 @@ public class SignalServiceAccountManager { byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber) + boolean discoverableByPhoneNumber, + int pniRegistrationId) { try { VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode, @@ -292,7 +296,8 @@ public class SignalServiceAccountManager { unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, - discoverableByPhoneNumber); + discoverableByPhoneNumber, + pniRegistrationId); return ServiceResponse.forResult(response, 200, null); } catch (IOException e) { return ServiceResponse.forUnknownError(e); @@ -320,7 +325,8 @@ public class SignalServiceAccountManager { byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber) + boolean discoverableByPhoneNumber, + int pniRegistrationId) { try { VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode, @@ -332,7 +338,8 @@ public class SignalServiceAccountManager { unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, - discoverableByPhoneNumber); + discoverableByPhoneNumber, + pniRegistrationId); return ServiceResponse.forResult(response, 200, null); } catch (IOException e) { return ServiceResponse.forUnknownError(e); @@ -346,7 +353,8 @@ public class SignalServiceAccountManager { boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, boolean discoverableByPhoneNumber, - byte[] encryptedDeviceName) + byte[] encryptedDeviceName, + int pniRegistrationId) throws IOException { AccountAttributes accountAttributes = new AccountAttributes( @@ -359,15 +367,16 @@ public class SignalServiceAccountManager { unrestrictedUnidentifiedAccess, capabilities, discoverableByPhoneNumber, - Base64.encodeBytes(encryptedDeviceName) + Base64.encodeBytes(encryptedDeviceName), + pniRegistrationId ); return this.pushServiceSocket.verifySecondaryDevice(verificationCode, accountAttributes); } - public ServiceResponse changeNumber(String code, String e164NewNumber, String registrationLock) { + public @Nonnull ServiceResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) { try { - VerifyAccountResponse response = this.pushServiceSocket.changeNumber(code, e164NewNumber, registrationLock); + VerifyAccountResponse response = this.pushServiceSocket.changeNumber(changePhoneNumberRequest); return ServiceResponse.forResult(response, 200, null); } catch (IOException e) { return ServiceResponse.forUnknownError(e); @@ -396,7 +405,8 @@ public class SignalServiceAccountManager { boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, boolean discoverableByPhoneNumber, - byte[] encryptedDeviceName) + byte[] encryptedDeviceName, + int pniRegistrationId) throws IOException { this.pushServiceSocket.setAccountAttributes( @@ -409,7 +419,8 @@ public class SignalServiceAccountManager { unrestrictedUnidentifiedAccess, capabilities, discoverableByPhoneNumber, - encryptedDeviceName + encryptedDeviceName, + pniRegistrationId ); } @@ -523,6 +534,13 @@ public class SignalServiceAccountManager { ServiceResponse serviceResponse; try { serviceResponse = single.blockingGet(); + } catch (RuntimeException e) { + Throwable cause = e.getCause(); + if (cause instanceof InterruptedException) { + throw new IOException("Interrupted", cause); + } else { + throw e; + } } catch (Exception e) { throw new RuntimeException("Unexpected exception when retrieving registered users!", e); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index db3b0bd77..ea707c227 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -141,6 +141,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.stream.Collectors; +import javax.annotation.Nonnull; + /** * The main interface for sending Signal Service messages. * @@ -156,8 +158,8 @@ public class SignalServiceMessageSender { private final SignalServiceAccountDataStore store; private final SignalSessionLock sessionLock; private final SignalServiceAddress localAddress; - private final int localDeviceId; - private final Optional eventListener; + private final int localDeviceId; + private final Optional eventListener; private final AttachmentService attachmentService; private final MessagingService messagingService; @@ -584,6 +586,24 @@ public class SignalServiceMessageSender { return sendMessage(localAddress, Optional.empty(), timestamp, envelopeContent, false, null, urgent); } + /** + * Create a device specific sync message that includes updated PNI details for that specific linked device. This message is + * sent to the server via the change number endpoint and not the normal sync message sending flow. + * + * @param deviceId - Device ID of linked device to build message for + * @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) + throws UntrustedIdentityException, IOException, InvalidKeyException + { + SyncMessage.Builder syncMessage = createSyncMessageBuilder().setPniChangeNumber(pniChangeNumber); + Content.Builder content = Content.newBuilder().setSyncMessage(syncMessage); + EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content.build(), ContentHint.IMPLICIT, Optional.empty()); + + return getEncryptedMessage(socket, localAddress, Optional.empty(), deviceId, envelopeContent); + } + public void setSoTimeoutMillis(long soTimeoutMillis) { socket.setSoTimeoutMillis(soTimeoutMillis); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java index d75697411..18f8c79a9 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/AccountAttributes.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. @@ -47,6 +47,9 @@ public class AccountAttributes { @JsonProperty private String name; + @JsonProperty + private int pniRegistrationId; + public AccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, @@ -56,7 +59,8 @@ public class AccountAttributes { boolean unrestrictedUnidentifiedAccess, Capabilities capabilities, boolean discoverableByPhoneNumber, - String name) + String name, + int pniRegistrationId) { this.signalingKey = signalingKey; this.registrationId = registrationId; @@ -70,6 +74,7 @@ public class AccountAttributes { this.capabilities = capabilities; this.discoverableByPhoneNumber = discoverableByPhoneNumber; this.name = name; + this.pniRegistrationId = pniRegistrationId; } public AccountAttributes() {} @@ -122,6 +127,10 @@ public class AccountAttributes { return name; } + public int getPniRegistrationId() { + return pniRegistrationId; + } + public static class Capabilities { @JsonProperty private boolean uuid; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/ChangePhoneNumberRequest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/ChangePhoneNumberRequest.java index b3da39bee..c6d6c7248 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/ChangePhoneNumberRequest.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/account/ChangePhoneNumberRequest.java @@ -1,6 +1,16 @@ 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.OutgoingPushMessage; +import org.whispersystems.signalservice.internal.util.JsonUtil; + +import java.util.List; +import java.util.Map; public final class ChangePhoneNumberRequest { @JsonProperty @@ -12,10 +22,37 @@ public final class ChangePhoneNumberRequest { @JsonProperty("reglock") private String registrationLock; - public ChangePhoneNumberRequest(String number, String code, String registrationLock) { - this.number = number; - this.code = code; - this.registrationLock = registrationLock; + @JsonProperty + @JsonSerialize(using = JsonUtil.IdentityKeySerializer.class) + @JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class) + private IdentityKey pniIdentityKey; + + @JsonProperty + private List deviceMessages; + + @JsonProperty + private Map devicePniSignedPrekeys; + + @JsonProperty + private Map pniRegistrationIds; + + public ChangePhoneNumberRequest() {} + + public ChangePhoneNumberRequest(String number, + String code, + String registrationLock, + IdentityKey pniIdentityKey, + List deviceMessages, + Map devicePniSignedPrekeys, + Map pniRegistrationIds) + { + this.number = number; + this.code = code; + this.registrationLock = registrationLock; + this.pniIdentityKey = pniIdentityKey; + this.deviceMessages = deviceMessages; + this.devicePniSignedPrekeys = devicePniSignedPrekeys; + this.pniRegistrationIds = pniRegistrationIds; } public String getNumber() { @@ -29,4 +66,20 @@ public final class ChangePhoneNumberRequest { public String getRegistrationLock() { return registrationLock; } + + public IdentityKey getPniIdentityKey() { + return pniIdentityKey; + } + + public List getDeviceMessages() { + return deviceMessages; + } + + public Map getDevicePniSignedPrekeys() { + return devicePniSignedPrekeys; + } + + public Map getPniRegistrationIds() { + return pniRegistrationIds; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceInfo.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceInfo.java index 3fedaec13..f81deb2cc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceInfo.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/multidevice/DeviceInfo.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2014-2016 Open Whisper Systems * * Licensed according to the LICENSE file in this repository. @@ -11,20 +11,20 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class DeviceInfo { @JsonProperty - private long id; + public int id; @JsonProperty - private String name; + public String name; @JsonProperty - private long created; + public long created; @JsonProperty - private long lastSeen; + public long lastSeen; public DeviceInfo() {} - public long getId() { + public int getId() { return id; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DeviceInfoList.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DeviceInfoList.java index 2ee522d84..6571ddfa0 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DeviceInfoList.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/DeviceInfoList.java @@ -9,7 +9,7 @@ import java.util.List; public class DeviceInfoList { @JsonProperty - private List devices; + public List devices; public DeviceInfoList() {} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java index f0e73abe3..6c51a28dc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PreKeyState.java @@ -23,6 +23,7 @@ public class PreKeyState { @JsonProperty private SignedPreKeyEntity signedPreKey; + public PreKeyState() {} public PreKeyState(List preKeys, SignedPreKeyEntity signedPreKey, IdentityKey identityKey) { this.preKeys = preKeys; @@ -30,4 +31,15 @@ public class PreKeyState { this.identityKey = identityKey; } + public IdentityKey getIdentityKey() { + return identityKey; + } + + public List getPreKeys() { + return preKeys; + } + + public SignedPreKeyEntity getSignedPreKey() { + return signedPreKey; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 8a45b929d..22d33f804 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -354,26 +354,31 @@ public class PushServiceSocket { return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); } - public VerifyAccountResponse verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, - String pin, String registrationLock, - byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, + public VerifyAccountResponse verifyAccountCode(String verificationCode, + String signalingKey, + int registrationId, + boolean fetchesMessages, + String pin, + String registrationLock, + byte[] unidentifiedAccessKey, + boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber) + boolean discoverableByPhoneNumber, + int pniRegistrationId) throws IOException { - AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, discoverableByPhoneNumber, null); + AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, discoverableByPhoneNumber, null, pniRegistrationId); String requestBody = JsonUtil.toJson(signalingKeyEntity); String responseBody = makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode), "PUT", requestBody); return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class); } - public VerifyAccountResponse changeNumber(String code, String e164NewNumber, String registrationLock) + public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) throws IOException { - ChangePhoneNumberRequest changePhoneNumberRequest = new ChangePhoneNumberRequest(e164NewNumber, code, registrationLock); - String requestBody = JsonUtil.toJson(changePhoneNumberRequest); - String responseBody = makeServiceRequest(CHANGE_NUMBER_PATH, "PUT", requestBody); + String requestBody = JsonUtil.toJson(changePhoneNumberRequest); + String responseBody = makeServiceRequest(CHANGE_NUMBER_PATH, "PUT", requestBody); return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class); } @@ -387,7 +392,8 @@ public class PushServiceSocket { boolean unrestrictedUnidentifiedAccess, AccountAttributes.Capabilities capabilities, boolean discoverableByPhoneNumber, - byte[] encryptedDeviceName) + byte[] encryptedDeviceName, + int pniRegistrationId) throws IOException { if (registrationLock != null && pin != null) { @@ -396,9 +402,18 @@ public class PushServiceSocket { String name = (encryptedDeviceName == null) ? null : Base64.encodeBytes(encryptedDeviceName); - AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin, registrationLock, - unidentifiedAccessKey, unrestrictedUnidentifiedAccess, capabilities, - discoverableByPhoneNumber, name); + AccountAttributes accountAttributes = new AccountAttributes(signalingKey, + registrationId, + fetchesMessages, + pin, + registrationLock, + unidentifiedAccessKey, + unrestrictedUnidentifiedAccess, + capabilities, + discoverableByPhoneNumber, + name, + pniRegistrationId); + makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SenderCertificate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SenderCertificate.java index fbf7dde9b..255282c22 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SenderCertificate.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/SenderCertificate.java @@ -20,7 +20,7 @@ public class SenderCertificate { @JsonProperty @JsonDeserialize(using = ByteArrayDesieralizer.class) @JsonSerialize(using = ByteArraySerializer.class) - private byte[] certificate; + public byte[] certificate; public SenderCertificate() {} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/VerifyAccountResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/VerifyAccountResponse.java index a2c77cf00..bb74ec964 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/VerifyAccountResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/VerifyAccountResponse.java @@ -5,13 +5,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class VerifyAccountResponse { @JsonProperty - private String uuid; + public String uuid; @JsonProperty - private String pni; + public String pni; @JsonProperty - private boolean storageCapable; + public boolean storageCapable; @JsonCreator public VerifyAccountResponse() {} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java index 3722e6d76..3a78e8047 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/WhoAmIResponse.java @@ -4,13 +4,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; public class WhoAmIResponse { @JsonProperty - private String uuid; + public String uuid; @JsonProperty - private String pni; + public String pni; @JsonProperty - private String number; + public String number; public String getAci() { return uuid; diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index 7c617bea0..ca9400504 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -569,6 +569,12 @@ message SyncMessage { } } + message PniChangeNumber { + optional bytes identityKeyPair = 1; // Serialized libsignal-client IdentityKeyPair + optional bytes signedPreKey = 2; // Serialized libsignal-client SignedPreKeyRecord + optional uint32 registrationId = 3; + } + optional Sent sent = 1; optional Contacts contacts = 2; optional Groups groups = 3; @@ -586,6 +592,7 @@ message SyncMessage { optional OutgoingPayment outgoingPayment = 15; repeated Viewed viewed = 16; optional PniIdentity pniIdentity = 17; + optional PniChangeNumber pniChangeNumber = 18; } message AttachmentPointer { diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java index fbf7056eb..0763dfb6d 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/account/AccountAttributesTest.java @@ -18,7 +18,8 @@ public final class AccountAttributesTest { false, new AccountAttributes.Capabilities(true, true, true, true, true, true, true, true, true), false, - null)); + null, + 321)); assertEquals("{\"signalingKey\":\"skey\"," + "\"registrationId\":123," + "\"voice\":true," + @@ -30,7 +31,7 @@ public final class AccountAttributesTest { "\"unrestrictedUnidentifiedAccess\":false," + "\"discoverableByPhoneNumber\":false," + "\"capabilities\":{\"uuid\":true,\"storage\":true,\"senderKey\":true,\"announcementGroup\":true,\"changeNumber\":true,\"stories\":true,\"giftBadges\":true,\"gv2-3\":true,\"gv1-migration\":true}," + - "\"name\":null}", json); + "\"name\":null,\"pniRegistrationId\":321}", json); } @Test