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