Add support for PNI registration ids and PNP change number.

fork-5.53.8
Cody Henthorne 2022-08-03 11:50:16 -04:00
rodzic 0d3ea22641
commit 83b97d274f
54 zmienionych plików z 1273 dodań i 188 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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))
}
}

Wyświetl plik

@ -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<ChangeNumberLockActivity>()
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()
}
}

Wyświetl plik

@ -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<Verb> = mutableListOf()
fun addMockWebRequestHandlers(vararg verbs: Verb) {
handlers.addAll(verbs)
}
fun clearHandlers() {
handlers.clear()
}
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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 <reified T> RecordedRequest.parsedRequestBody(): T {
val bodyString = String(body.readByteArray())
return JsonUtils.fromJson(bodyString, T::class.java)
}

Wyświetl plik

@ -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<VerifyAccountResponse> = 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 <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit): ActivityScenario<T> {
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario<T> {
return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent))
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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 : Any?> T.assertIsNull() {
assertThat(this, nullValue())
}
fun <T : Any?> T.assertIsNotNull() {
assertThat(this, notNullValue())
}
infix fun <T : Any> T.assertIs(expected: T) {
assertThat(this, `is`(expected))
}
infix fun <T : Any> T.assertIsNot(expected: T) {
assertThat(this, not(`is`(expected)))
}
infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}

Wyświetl plik

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<application
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic"
tools:ignore="UnusedAttribute" />
</manifest>

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -1,11 +1,21 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.state.SignalProtocolStore
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.CertificateType
import org.thoughtcrime.securesms.keyvalue.SignalStore
@ -17,22 +27,45 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import java.io.IOException
import java.security.MessageDigest
import java.security.SecureRandom
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
class ChangeNumberRepository(private val context: Context) {
class ChangeNumberRepository(private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()) {
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
fun ensureDecryptionsDrained(): Completable {
return Completable.create { emitter ->
ApplicationDependencies
.getIncomingMessageObserver()
.addDecryptionDrainedListener {
emitter.onComplete()
}
}.subscribeOn(Schedulers.io())
}
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
.subscribeOn(Schedulers.io())
return Single.fromCallable {
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null)
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
accountManager.changeNumber(request)
}.subscribeOn(Schedulers.io())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
}
fun changeNumber(
@ -45,8 +78,11 @@ class ChangeNumberRepository(private val context: Context) {
try {
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock)
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(request)
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
} catch (e: KeyBackupSystemWrongPinException) {
ServiceResponse.forExecutionError(e)
@ -56,6 +92,7 @@ class ChangeNumberRepository(private val context: Context) {
ServiceResponse.forExecutionError(e)
}
}.subscribeOn(Schedulers.io())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
}
@Suppress("UsePropertyAccessSyntax")
@ -86,6 +123,49 @@ class ChangeNumberRepository(private val context: Context) {
SignalStore.account().setE164(e164)
SignalStore.account().setPni(pni)
ApplicationDependencies.getGroupsV2Authorization().clear()
val metadata: PendingChangeNumberMetadata? = SignalStore.misc().pendingChangeNumberMetadata
if (metadata == null) {
Log.w(TAG, "No change number metadata, this shouldn't happen")
throw AssertionError("No change number metadata")
}
val originalPni = ServiceId.fromByteString(metadata.previousPni)
if (originalPni == pni) {
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
} else {
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
val pniRegistrationId = metadata.pniRegistrationId
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
SignalStore.account().pniRegistrationId = pniRegistrationId
SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(pniProtocolStore, pniMetadataStore)
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
accountManager.setPreKeys(ServiceIdType.PNI, pniProtocolStore.identityKeyPair.publicKey, signedPreKey, oneTimePreKeys)
pniMetadataStore.isSignedPreKeyRegistered = true
pniProtocolStore.identities().saveIdentityWithoutSideEffects(
Recipient.self().id,
pniProtocolStore.identityKeyPair.publicKey,
IdentityDatabase.VerifiedStatus.VERIFIED,
true,
System.currentTimeMillis(),
true
)
}
Recipient.self().live().refresh()
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.closeConnections()
ApplicationDependencies.getIncomingMessageObserver()
@ -112,4 +192,66 @@ class ChangeNumberRepository(private val context: Context) {
}
}.subscribeOn(Schedulers.io())
}
@Suppress("UsePropertyAccessSyntax")
@WorkerThread
private fun createChangeNumberRequest(
code: String,
newE164: String,
registrationLock: String?
): ChangeNumberRequestData {
val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore: PreKeyMetadataStore = SignalStore.account().pniPreKeys
val devices: List<DeviceInfo> = accountManager.getDevices()
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
val deviceMessages = mutableListOf<OutgoingPushMessage>()
val devicePniSignedPreKeys = mutableMapOf<String, SignedPreKeyEntity>()
val pniRegistrationIds = mutableMapOf<String, Int>()
val primaryDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID.toString()
for (device in devices) {
val deviceId = device.id.toString()
// Signed Prekeys
val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
PreKeyUtil.generateAndStoreSignedPreKey(pniProtocolStore, pniMetadataStore, pniIdentity.privateKey, false)
} else {
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
}
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
// Registration Ids
var pniRegistrationId = -1
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
pniRegistrationId = KeyHelper.generateRegistrationId(false)
}
pniRegistrationIds[deviceId] = pniRegistrationId
// Device Messages
if (deviceId != primaryDeviceId) {
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
.setRegistrationId(pniRegistrationId)
.build()
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(device.id, pniChangeNumber)
}
}
val request = ChangePhoneNumberRequest(newE164, code, registrationLock, pniIdentity.publicKey, deviceMessages, devicePniSignedPreKeys, pniRegistrationIds)
val metadata = PendingChangeNumberMetadata.newBuilder()
.setPreviousPni(SignalStore.account().pni!!.toByteString())
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
.build()
return ChangeNumberRequestData(request, metadata)
}
data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
}

Wyświetl plik

@ -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()
}
)
}
}
}

Wyświetl plik

@ -9,6 +9,7 @@ import androidx.lifecycle.ViewModel
import androidx.savedstate.SavedStateRegistryOwner
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@ -107,6 +108,10 @@ class ChangeNumberViewModel(
}
}
fun ensureDecryptionsDrained(): Completable {
return changeNumberRepository.ensureDecryptionsDrained()
}
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
return super.verifyCodeWithoutRegistrationLock(code)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
@ -122,6 +127,7 @@ class ChangeNumberViewModel(
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
return if (processor.hasResult() || processor.isServerSentError()) {
SignalStore.misc().unlockChangeNumber()
SignalStore.misc().clearPendingChangeNumberMetadata()
Single.just(processor)
} else {
changeNumberRepository.whoAmI()
@ -129,6 +135,7 @@ class ChangeNumberViewModel(
if (Objects.equals(whoAmI.number, localNumber)) {
Log.i(TAG, "Local and remote numbers match, we can unlock.")
SignalStore.misc().unlockChangeNumber()
SignalStore.misc().clearPendingChangeNumberMetadata()
}
processor
}
@ -172,7 +179,7 @@ class ChangeNumberViewModel(
val viewModel = ChangeNumberViewModel(
localNumber = localNumber,
changeNumberRepository = ChangeNumberRepository(context),
changeNumberRepository = ChangeNumberRepository(),
savedState = handle,
password = password,
verifyAccountRepository = VerifyAccountRepository(context),

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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.")

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.keyvalue
import org.signal.core.util.ByteSerializer
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
/**
* Serialize [PendingChangeNumberMetadata]
*/
object PendingChangeNumberMetadataSerializer : ByteSerializer<PendingChangeNumberMetadata> {
override fun serialize(data: PendingChangeNumberMetadata): ByteArray = data.toByteArray()
override fun deserialize(data: ByteArray): PendingChangeNumberMetadata = PendingChangeNumberMetadata.parseFrom(data)
}

Wyświetl plik

@ -1,9 +1,11 @@
package org.thoughtcrime.securesms.keyvalue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.core.util.ByteSerializer;
import org.signal.core.util.StringSerializer;
import org.thoughtcrime.securesms.database.model.databaseprotos.SignalStoreList;
@ -51,6 +53,15 @@ abstract class SignalStoreValues {
return store.getBlob(key, defaultValue);
}
<T> T getObject(@NonNull String key, @Nullable T defaultValue, @NonNull ByteSerializer<T> serializer) {
byte[] blob = store.getBlob(key, null);
if (blob == null) {
return defaultValue;
} else {
return serializer.deserialize(blob);
}
}
<T> List<T> getList(@NonNull String key, @NonNull StringSerializer<T> serializer) {
byte[] blob = getBlob(key, null);
if (blob == null) {
@ -94,6 +105,10 @@ abstract class SignalStoreValues {
store.beginWrite().putString(key, value).apply();
}
<T> void putObject(@NonNull String key, T value, @NonNull ByteSerializer<T> serializer) {
putBlob(key, serializer.serialize(value));
}
<T> void putList(@NonNull String key, @NonNull List<T> values, @NonNull StringSerializer<T> serializer) {
putBlob(key, SignalStoreList.newBuilder()
.addAllContents(values.stream()

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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,

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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);

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -238,4 +238,11 @@ message GiftBadge {
message SignalStoreList {
repeated string contents = 1;
}
}
message PendingChangeNumberMetadata {
bytes previousPni = 1;
bytes pniIdentityKeyPair = 2;
int32 pniRegistrationId = 3;
int32 pniSignedPreKeyId = 4;
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -11,3 +11,5 @@ interface Serializer<T, R> {
interface StringSerializer<T> : Serializer<T, String>
interface LongSerializer<T> : Serializer<T, Long>
interface ByteSerializer<T> : Serializer<T, ByteArray>

Wyświetl plik

@ -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')
}

Wyświetl plik

@ -2795,6 +2795,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="916cc0257f6a66a18b7f7ad6ec510a1002fc5721cdb13e294edbb8475a2a80c5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.jakewharton.android.repackaged" name="dalvik-dx" version="9.0.0_r3">
<artifact name="dalvik-dx-9.0.0_r3.jar">
<sha256 value="b29c1c21e52ed6238cd3fed39d880a17ecf2360118604548cea8821be6801e1c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.jpardogo.materialtabstrip" name="library" version="1.0.9">
<artifact name="library-1.0.9.aar">
<sha256 value="c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa" origin="Generated by Gradle"/>
@ -3103,6 +3108,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="b20bd74ba01b55b30d6b7d10b9373f2a324b1f3638ecda0275c93e454223c7c8" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="mockwebserver" version="3.12.13">
<artifact name="mockwebserver-3.12.13.jar">
<sha256 value="ec92604c885f37eede54cd8504c871a5163616d94074f34ce27184845e7cfd4c" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.squareup.okhttp3" name="okhttp" version="3.12.13">
<artifact name="okhttp-3.12.13.jar">
<sha256 value="508234e024ef7e270ab1a6d5b356f5b98e786511239ca986d684fd1e2cf7bc82" origin="Generated by Gradle"/>
@ -3740,6 +3750,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="150ce4369f02782c78a04f359dc2b4f62e7521cd836d9f8a20bb481aeec4c500" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy" version="1.12.10">
<artifact name="byte-buddy-1.12.10.jar">
<sha256 value="1a1ac9ce65eddcea54ead958387bb0b3863d02a2ffe856ab6a57ac79737c19cf" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy" version="1.12.8">
<artifact name="byte-buddy-1.12.8.jar">
<sha256 value="42ba43dcccd8d9d77bfe8776a83e72b67f1fa52c4038a98629e7d288b648da4e" origin="Generated by Gradle"/>
@ -3756,6 +3771,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="19cec6847112d179187327fa374fd19390f034b7baf1390ada16d3b0016d10b6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.12.10">
<artifact name="byte-buddy-agent-1.12.10.jar">
<sha256 value="5e8606d14a844c1ec70d2eb8f50c4009fb16138905dee8ca50a328116c041257" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-agent" version="1.12.8">
<artifact name="byte-buddy-agent-1.12.8.jar">
<sha256 value="18faf4f33893c3e883a3220adfdab25d6ceff9e46b1ad760a5b6f83df9373307" origin="Generated by Gradle"/>
@ -3772,6 +3792,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="220162fbfd04a47104ad3408f98a97727cf5f38e78cd786949f8c5fe9c485c6e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-android" version="1.12.10">
<artifact name="byte-buddy-android-1.12.10.jar">
<sha256 value="5e2c0b2ddb02e51fe95e4a58052fc7af91e566b627fe52f4e15699baa1f686d7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="net.bytebuddy" name="byte-buddy-parent" version="1.12.8">
<artifact name="byte-buddy-parent-1.12.8.pom">
<sha256 value="9e5ac6370596087eb97a0f639ed655dbfe6df2deaa8422fba51946076a2a2751" origin="Generated by Gradle"/>
@ -5229,6 +5254,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="8fc84f36ce6da6ce8c893b6538199a7f69a69a0706d9b17a3ee6a3a09452eed6" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito" name="mockito-android" version="4.6.1">
<artifact name="mockito-android-4.6.1.jar">
<sha256 value="c631906a7909199f47c4e0d68137bd37a909d2577f06548fe2642b7189cf6358" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito" name="mockito-core" version="2.19.0">
<artifact name="mockito-core-2.19.0.jar">
<sha256 value="d6ac2e04164c5d5c89e73838dc1c8b3856ca6582d3f2daf91816fd9d7ba3c9a9" origin="Generated by Gradle"/>
@ -5245,6 +5275,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="3844e38af447014bb2c7a68b421be65f8c7d5a201971bda663302367c14297d3" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito" name="mockito-core" version="4.6.1">
<artifact name="mockito-core-4.6.1.jar">
<sha256 value="ee3b91cdf4c23cff92960c32364371c683ee6415f1ec4678317bcea79c9f9819" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito" name="mockito-inline" version="4.4.0">
<artifact name="mockito-inline-4.4.0.jar">
<sha256 value="ee52e1c299a632184fba274a9370993e09140429f5e516e6c5570fd6574b297f" origin="Generated by Gradle"/>
@ -5253,6 +5288,11 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="21de15a4e9e446d8a1219926c14b48ac8d2828329a69494bce100d65719b27e0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito" name="mockito-inline" version="4.6.1">
<artifact name="mockito-inline-4.6.1.jar">
<sha256 value="ee52e1c299a632184fba274a9370993e09140429f5e516e6c5570fd6574b297f" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.mockito.kotlin" name="mockito-kotlin" version="4.0.0">
<artifact name="mockito-kotlin-4.0.0.jar">
<sha256 value="046eabba9c38816f75114163ac5074630f335dcdeeac52f228ce71c732c3d75f" origin="Generated by Gradle"/>

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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<VerifyAccountResponse> changeNumber(String code, String e164NewNumber, String registrationLock) {
public @Nonnull ServiceResponse<VerifyAccountResponse> 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<CdsiV2Service.Response> 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);
}

Wyświetl plik

@ -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> eventListener;
private final int localDeviceId;
private final Optional<EventListener> 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);
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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<OutgoingPushMessage> deviceMessages;
@JsonProperty
private Map<String, SignedPreKeyEntity> devicePniSignedPrekeys;
@JsonProperty
private Map<String, Integer> pniRegistrationIds;
public ChangePhoneNumberRequest() {}
public ChangePhoneNumberRequest(String number,
String code,
String registrationLock,
IdentityKey pniIdentityKey,
List<OutgoingPushMessage> deviceMessages,
Map<String, SignedPreKeyEntity> devicePniSignedPrekeys,
Map<String, Integer> 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<OutgoingPushMessage> getDeviceMessages() {
return deviceMessages;
}
public Map<String, SignedPreKeyEntity> getDevicePniSignedPrekeys() {
return devicePniSignedPrekeys;
}
public Map<String, Integer> getPniRegistrationIds() {
return pniRegistrationIds;
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -9,7 +9,7 @@ import java.util.List;
public class DeviceInfoList {
@JsonProperty
private List<DeviceInfo> devices;
public List<DeviceInfo> devices;
public DeviceInfoList() {}

Wyświetl plik

@ -23,6 +23,7 @@ public class PreKeyState {
@JsonProperty
private SignedPreKeyEntity signedPreKey;
public PreKeyState() {}
public PreKeyState(List<PreKeyEntity> 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<PreKeyEntity> getPreKeys() {
return preKeys;
}
public SignedPreKeyEntity getSignedPreKey() {
return signedPreKey;
}
}

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -20,7 +20,7 @@ public class SenderCertificate {
@JsonProperty
@JsonDeserialize(using = ByteArrayDesieralizer.class)
@JsonSerialize(using = ByteArraySerializer.class)
private byte[] certificate;
public byte[] certificate;
public SenderCertificate() {}

Wyświetl plik

@ -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() {}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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