diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 9543f856c0..e6ba07b384 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -47,7 +47,6 @@ import androidx.transition.TransitionManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import org.signal.core.util.concurrent.JvmRxExtensions; import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; @@ -721,12 +720,7 @@ public final class ContactSelectionListFragment extends LoggingFragment { AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext()); SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { - try { - return JvmRxExtensions.safeBlockingGet(UsernameRepository.fetchAciForUsername(UsernameUtil.sanitizeUsernameFromSearch(username))); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted?", e); - return UsernameAciFetchResult.NetworkError.INSTANCE; - } + return UsernameRepository.fetchAciForUsername(UsernameUtil.sanitizeUsernameFromSearch(username)); }, result -> { loadingDialog.dismiss(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt index 7e2063618f..267c514a20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataRepository.kt @@ -5,22 +5,22 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers -import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.util.JsonUtils -import org.whispersystems.signalservice.api.SignalServiceAccountManager -import java.io.IOException +import org.whispersystems.signalservice.api.NetworkResult -class ExportAccountDataRepository( - private val accountManager: SignalServiceAccountManager = AppDependencies.signalServiceAccountManager -) { +class ExportAccountDataRepository { fun downloadAccountDataReport(exportAsJson: Boolean): Single { return Single.create { - try { - it.onSuccess(generateAccountDataReport(accountManager.accountDataReport, exportAsJson)) - } catch (e: IOException) { - it.onError(e) + when (val result = SignalNetwork.account.accountDataReport()) { + is NetworkResult.Success -> { + it.onSuccess(generateAccountDataReport(result.result, exportAsJson)) + } + else -> { + it.onError(result.getCause()!!) + } } }.subscribeOn(Schedulers.io()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt index f27fb93dcc..6a9812a5bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/changenumber/ChangeNumberRepository.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.keyvalue.CertificateType import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.pin.SvrRepository import org.thoughtcrime.securesms.pin.SvrWrongPinException import org.thoughtcrime.securesms.recipients.Recipient @@ -266,7 +267,7 @@ class ChangeNumberRepository( SignalStore.misc.setPendingChangeNumberMetadata(metadata) withContext(Dispatchers.IO) { - result = accountManager.registrationApi.changeNumber(request) + result = SignalNetwork.account.changeNumber(request) } val possibleError = result.getCause() as? MismatchedDevicesException diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsRepository.kt index 3d6606998b..75d58ada59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsRepository.kt @@ -9,12 +9,13 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.NetworkResultUtil import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException import java.io.IOException -import java.util.Optional import java.util.concurrent.ExecutionException private val TAG = Log.tag(AdvancedPrivacySettingsRepository::class.java) @@ -24,9 +25,8 @@ class AdvancedPrivacySettingsRepository(private val context: Context) { fun disablePushMessages(consumer: (DisablePushMessagesResult) -> Unit) { SignalExecutors.BOUNDED.execute { val result = try { - val accountManager = AppDependencies.signalServiceAccountManager try { - accountManager.setGcmId(Optional.empty()) + NetworkResultUtil.toBasicLegacy(SignalNetwork.account.clearFcmToken()) } catch (e: AuthorizationFailedException) { Log.w(TAG, e) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java index 4fe1a63bde..637f4b6f87 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java @@ -16,7 +16,9 @@ import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.groups.GroupManager; +import org.thoughtcrime.securesms.net.SignalNetwork; import org.thoughtcrime.securesms.util.ServiceUtil; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.internal.EmptyResponse; import org.whispersystems.signalservice.internal.ServiceResponse; @@ -103,7 +105,7 @@ class DeleteAccountRepository { Log.i(TAG, "deleteAccount: attempting to delete account from server..."); try { - AppDependencies.getSignalServiceAccountManager().deleteAccount(); + NetworkResultUtil.toBasicLegacy(SignalNetwork.account().deleteAccount()); } catch (IOException e) { Log.w(TAG, "deleteAccount: failed to delete account from signal service", e); onDeleteAccountEvent.accept(DeleteAccountEvent.ServerDeletionFailed.INSTANCE); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index 316cd51d4a..4d812f1e28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceDataStore import org.whispersystems.signalservice.api.SignalServiceMessageReceiver import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.account.AccountApi import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations @@ -52,6 +53,7 @@ import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService import org.whispersystems.signalservice.api.storage.StorageServiceApi +import org.whispersystems.signalservice.api.username.UsernameApi import org.whispersystems.signalservice.api.websocket.SignalWebSocket import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration @@ -311,6 +313,12 @@ object AppDependencies { val storageServiceApi: StorageServiceApi get() = networkModule.storageServiceApi + val accountApi: AccountApi + get() = networkModule.accountApi + + val usernameApi: UsernameApi + get() = networkModule.usernameApi + @JvmStatic val okHttpClient: OkHttpClient get() = networkModule.okHttpClient @@ -338,7 +346,7 @@ object AppDependencies { interface Provider { fun providePushServiceSocket(signalServiceConfiguration: SignalServiceConfiguration, groupsV2Operations: GroupsV2Operations): PushServiceSocket fun provideGroupsV2Operations(signalServiceConfiguration: SignalServiceConfiguration): GroupsV2Operations - fun provideSignalServiceAccountManager(pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager + fun provideSignalServiceAccountManager(authWebSocket: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager fun provideSignalServiceMessageSender(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, protocolStore: SignalServiceDataStore, pushServiceSocket: PushServiceSocket): SignalServiceMessageSender fun provideSignalServiceMessageReceiver(pushServiceSocket: PushServiceSocket): SignalServiceMessageReceiver fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess @@ -382,5 +390,7 @@ object AppDependencies { fun provideStorageServiceApi(pushServiceSocket: PushServiceSocket): StorageServiceApi fun provideAuthWebSocket(signalServiceConfigurationSupplier: Supplier, libSignalNetworkSupplier: Supplier): SignalWebSocket.AuthenticatedWebSocket fun provideUnauthWebSocket(signalServiceConfigurationSupplier: Supplier, libSignalNetworkSupplier: Supplier): SignalWebSocket.UnauthenticatedWebSocket + fun provideAccountApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): AccountApi + fun provideUsernameApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): UsernameApi } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 35cbe11d58..2160efa323 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -80,6 +80,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.account.AccountApi; import org.whispersystems.signalservice.api.archive.ArchiveApi; import org.whispersystems.signalservice.api.attachment.AttachmentApi; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; @@ -93,6 +94,7 @@ import org.whispersystems.signalservice.api.services.CallLinksService; import org.whispersystems.signalservice.api.services.DonationsService; import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.storage.StorageServiceApi; +import org.whispersystems.signalservice.api.username.UsernameApi; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -140,8 +142,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { } @Override - public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull PushServiceSocket pushServiceSocket, @NonNull GroupsV2Operations groupsV2Operations) { - return new SignalServiceAccountManager(pushServiceSocket, groupsV2Operations); + public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(@NonNull AccountApi accountApi, @NonNull PushServiceSocket pushServiceSocket, @NonNull GroupsV2Operations groupsV2Operations) { + return new SignalServiceAccountManager(accountApi, pushServiceSocket, groupsV2Operations); } @Override @@ -492,6 +494,16 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { return new StorageServiceApi(pushServiceSocket); } + @Override + public @NonNull AccountApi provideAccountApi(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket) { + return new AccountApi(authWebSocket); + } + + @Override + public @NonNull UsernameApi provideUsernameApi(@NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket) { + return new UsernameApi(unauthWebSocket); + } + @VisibleForTesting static class DynamicCredentialsProvider implements CredentialsProvider { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt index d69535a151..3ca1a53b22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.push.SignalServiceTrustStore import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceMessageReceiver import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.account.AccountApi import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations @@ -37,6 +38,7 @@ import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService import org.whispersystems.signalservice.api.storage.StorageServiceApi +import org.whispersystems.signalservice.api.username.UsernameApi import org.whispersystems.signalservice.api.util.Tls12SocketFactory import org.whispersystems.signalservice.api.websocket.SignalWebSocket import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState @@ -83,7 +85,7 @@ class NetworkDependenciesModule( } val signalServiceAccountManager: SignalServiceAccountManager by lazy { - provider.provideSignalServiceAccountManager(pushServiceSocket, groupsV2Operations) + provider.provideSignalServiceAccountManager(accountApi, pushServiceSocket, groupsV2Operations) } val libsignalNetwork: Network by lazy { @@ -157,6 +159,14 @@ class NetworkDependenciesModule( provider.provideStorageServiceApi(pushServiceSocket) } + val accountApi: AccountApi by lazy { + provider.provideAccountApi(authWebSocket) + } + + val usernameApi: UsernameApi by lazy { + provider.provideUsernameApi(unauthWebSocket) + } + val okHttpClient: OkHttpClient by lazy { OkHttpClient.Builder() .addInterceptor(StandardUserAgentInterceptor()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java index 5d7e3c1a93..8af65ea7fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/FcmRefreshJob.java @@ -38,9 +38,11 @@ import org.thoughtcrime.securesms.gcm.FcmUtil; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.net.SignalNetwork; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationIds; import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import java.io.IOException; @@ -100,7 +102,7 @@ public class FcmRefreshJob extends BaseJob { Log.i(TAG, "Token didn't change."); } - AppDependencies.getSignalServiceAccountManager().setGcmId(token); + NetworkResultUtil.toBasicLegacy(SignalNetwork.account().setFcmToken(token.get())); SignalStore.account().setFcmToken(token.get()); } else { throw new RetryLaterException(new IOException("Failed to retrieve a token.")); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt index 3ca64c70ef..d0a5355756 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PnpInitializeDevicesJob.kt @@ -19,15 +19,16 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignedPreKeyEntity import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity +import org.whispersystems.signalservice.internal.push.MismatchedDevices import org.whispersystems.signalservice.internal.push.OutgoingPushMessage import org.whispersystems.signalservice.internal.push.SyncMessage import org.whispersystems.signalservice.internal.push.VerifyAccountResponse -import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException import java.io.IOException import java.security.SecureRandom @@ -112,7 +113,6 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base } private fun initializeDevices(newE164: String): Single> { - val accountManager = AppDependencies.signalServiceAccountManager val messageSender = AppDependencies.signalServiceMessageSender return Single.fromCallable { @@ -125,15 +125,25 @@ class PnpInitializeDevicesJob private constructor(parameters: Parameters) : Base newE164 = newE164 ) - distributionResponse = accountManager.registrationApi.distributePniKeys(request) - - if (distributionResponse is NetworkResult.StatusCodeError && - distributionResponse.exception is MismatchedDevicesException - ) { - messageSender.handleChangeNumberMismatchDevices((distributionResponse.exception as MismatchedDevicesException).mismatchedDevices) - attempts++ - } else { - completed = true + distributionResponse = SignalNetwork.account.distributePniKeys(request) + when (val result = distributionResponse) { + is NetworkResult.Success -> completed = true + is NetworkResult.StatusCodeError -> { + when (result.code) { + 409 -> { + val mismatchedDevices: MismatchedDevices? = result.parseJsonBody() + if (mismatchedDevices != null) { + messageSender.handleChangeNumberMismatchDevices(mismatchedDevices) + } else { + Log.w(TAG, "Unable to parse mismatched devices", result.exception) + } + attempts++ + } + else -> completed = true + } + } + is NetworkResult.NetworkError -> attempts++ + is NetworkResult.ApplicationError -> completed = true } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index 81e2fab135..42ca686b63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -16,10 +16,12 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SvrValues; +import org.thoughtcrime.securesms.net.SignalNetwork; import org.thoughtcrime.securesms.registration.data.RegistrationRepository; import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher; import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -125,7 +127,7 @@ public class RefreshAttributesJob extends BaseJob { recoveryPassword ); - AppDependencies.getSignalServiceAccountManager().setAccountAttributes(accountAttributes); + NetworkResultUtil.toBasicLegacy(SignalNetwork.account().setAccountAttributes(accountAttributes)); hasRefreshedThisAppCycle = true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index c3ba0a0f6c..3e6434016f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -23,12 +23,14 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.net.SignalNetwork; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.manage.UsernameRepository; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.signalservice.api.NetworkResultUtil; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -328,7 +330,7 @@ public class RefreshOwnProfileJob extends BaseJob { UsernameLinkComponents localUsernameLink = SignalStore.account().getUsernameLink(); if (localUsernameLink != null) { - byte[] remoteEncryptedUsername = AppDependencies.getSignalServiceAccountManager().getEncryptedUsernameFromLinkServerId(localUsernameLink.getServerId()); + byte[] remoteEncryptedUsername = NetworkResultUtil.toBasicLegacy(SignalNetwork.username().getEncryptedUsernameFromLinkServerId(localUsernameLink.getServerId())); Username.UsernameLink combinedLink = new Username.UsernameLink(localUsernameLink.getEntropy(), remoteEncryptedUsername); Username remoteUsername = Username.fromLink(combinedLink); diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt index e09a0bb1f3..5f828c3773 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/net/SignalNetwork.kt @@ -6,16 +6,23 @@ package org.thoughtcrime.securesms.net import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.whispersystems.signalservice.api.account.AccountApi import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.keys.KeysApi import org.whispersystems.signalservice.api.link.LinkDeviceApi import org.whispersystems.signalservice.api.storage.StorageServiceApi +import org.whispersystems.signalservice.api.username.UsernameApi /** * A convenient way to access network operations, similar to [org.thoughtcrime.securesms.database.SignalDatabase] and [org.thoughtcrime.securesms.keyvalue.SignalStore]. */ object SignalNetwork { + @JvmStatic + @get:JvmName("account") + val account: AccountApi + get() = AppDependencies.accountApi + val archive: ArchiveApi get() = AppDependencies.archiveApi @@ -30,4 +37,9 @@ object SignalNetwork { val storageService: StorageServiceApi get() = AppDependencies.storageServiceApi + + @JvmStatic + @get:JvmName("username") + val username: UsernameApi + get() = AppDependencies.usernameApi } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt index 86b85d6980..4377813ee9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt @@ -23,8 +23,10 @@ import org.thoughtcrime.securesms.jobs.Svr3MirrorJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.lock.v2.PinKeyboardType import org.thoughtcrime.securesms.megaphone.Megaphones +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet import org.thoughtcrime.securesms.registrationv3.ui.restore.StorageServiceRestore +import org.whispersystems.signalservice.api.NetworkResultUtil import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.svr.SecureValueRecovery @@ -360,7 +362,7 @@ object SvrRepository { check(SignalStore.svr.hasOptedInWithAccess() && !SignalStore.svr.hasOptedOut()) { "Must have a PIN to set a registration lock!" } Log.i(TAG, "[enableRegistrationLockForUserWithPin] Enabling registration lock.", true) - AppDependencies.signalServiceAccountManager.enableRegistrationLock(SignalStore.svr.masterKey) + NetworkResultUtil.toBasicLegacy(SignalNetwork.account.enableRegistrationLock(SignalStore.svr.masterKey.deriveRegistrationLock())) SignalStore.svr.isRegistrationLockEnabled = true Log.i(TAG, "[enableRegistrationLockForUserWithPin] Registration lock successfully enabled.", true) } @@ -374,7 +376,7 @@ object SvrRepository { check(SignalStore.svr.hasOptedInWithAccess() && !SignalStore.svr.hasOptedOut()) { "Must have a PIN to disable registration lock!" } Log.i(TAG, "[disableRegistrationLockForUserWithPin] Disabling registration lock.", true) - AppDependencies.signalServiceAccountManager.disableRegistrationLock() + NetworkResultUtil.toBasicLegacy(SignalNetwork.account.disableRegistrationLock()) SignalStore.svr.isRegistrationLockEnabled = false Log.i(TAG, "[disableRegistrationLockForUserWithPin] Registration lock successfully disabled.", true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt index d1cfed0bc0..d4361c9ecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameRepository.kt @@ -15,23 +15,18 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.NetworkUtil import org.thoughtcrime.securesms.util.UsernameUtil +import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.UsernameLinkComponents -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException -import org.whispersystems.signalservice.api.push.exceptions.RateLimitException -import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssociatedWithAnAccountException -import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException -import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException -import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException import org.whispersystems.signalservice.api.util.Usernames import org.whispersystems.signalservice.api.util.UuidUtil import org.whispersystems.signalservice.api.util.toByteArray -import java.io.IOException import java.util.UUID /** @@ -200,26 +195,30 @@ object UsernameRepository { return Single .fromCallable { - try { - SignalStore.account.usernameLink = null + SignalStore.account.usernameLink = null - Log.d(TAG, "[createOrResetUsernameLink] Creating username link...") - val components = accountManager.createUsernameLink(username) - SignalStore.account.usernameLink = components + Log.d(TAG, "[createOrResetUsernameLink] Creating username link...") - if (SignalStore.account.usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) { - SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC - SignalStore.account.usernameSyncErrorCount = 0 + val usernameLink = username.generateLink() + when (val result = SignalNetwork.account.createUsernameLink(usernameLink)) { + is NetworkResult.Success -> { + SignalStore.account.usernameLink = result.result + + if (SignalStore.account.usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) { + SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC + SignalStore.account.usernameSyncErrorCount = 0 + } + + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + Log.d(TAG, "[createOrResetUsernameLink] Username link created.") + + UsernameLinkResetResult.Success(result.result) + } + else -> { + Log.w(TAG, "[createOrResetUsernameLink] Failed to rotate the username!", result.getCause()) + UsernameLinkResetResult.NetworkError } - - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - Log.d(TAG, "[createOrResetUsernameLink] Username link created.") - - UsernameLinkResetResult.Success(components) - } catch (e: IOException) { - Log.w(TAG, "[createOrResetUsernameLink] Failed to rotate the username!", e) - UsernameLinkResetResult.NetworkError } } .subscribeOn(Schedulers.io()) @@ -234,53 +233,72 @@ object UsernameRepository { return Single .fromCallable { - var username: Username? = null + val encryptedUsername = when (val result = SignalNetwork.username.getEncryptedUsernameFromLinkServerId(components.serverId)) { + is NetworkResult.Success -> result.result + is NetworkResult.StatusCodeError -> { + return@fromCallable when (result.code) { + 404 -> UsernameLinkConversionResult.NotFound(null) + 422 -> UsernameLinkConversionResult.Invalid + else -> UsernameLinkConversionResult.NetworkError + } + } + is NetworkResult.NetworkError -> return@fromCallable UsernameLinkConversionResult.NetworkError + is NetworkResult.ApplicationError -> throw result.throwable + } - try { - val encryptedUsername: ByteArray = accountManager.getEncryptedUsernameFromLinkServerId(components.serverId) - val link = Username.UsernameLink(components.entropy, encryptedUsername) + val link = Username.UsernameLink(components.entropy, encryptedUsername) + val username: Username = try { + Username.fromLink(link) + } catch (e: BaseUsernameException) { + Log.w(TAG, "[convertLinkToUsername] Bad username conversion.", e) + return@fromCallable UsernameLinkConversionResult.Invalid + } - username = Username.fromLink(link) - - val aci = accountManager.getAciByUsername(username) - - UsernameLinkConversionResult.Success(username, aci) - } catch (e: IOException) { - Log.w(TAG, "[convertLinkToUsername] Failed to lookup user.", e) - - if (e is NonSuccessfulResponseCodeException) { - when (e.code) { + when (val result = SignalNetwork.username.getAciByUsername(username)) { + is NetworkResult.Success -> UsernameLinkConversionResult.Success(username, result.result) + is NetworkResult.StatusCodeError -> { + Log.w(TAG, "[convertLinkToUsername] Failed to lookup user.", result.exception) + when (result.code) { 404 -> UsernameLinkConversionResult.NotFound(username) 422 -> UsernameLinkConversionResult.Invalid else -> UsernameLinkConversionResult.NetworkError } - } else { + } + is NetworkResult.NetworkError -> { + Log.w(TAG, "[convertLinkToUsername] Failed to lookup user.", result.exception) UsernameLinkConversionResult.NetworkError } - } catch (e: BaseUsernameException) { - Log.w(TAG, "[convertLinkToUsername] Bad username conversion.", e) - UsernameLinkConversionResult.Invalid + is NetworkResult.ApplicationError -> throw result.throwable } } .subscribeOn(Schedulers.io()) } @JvmStatic - fun fetchAciForUsername(username: String): Single { - return Single.fromCallable { - try { - val aci: ACI = AppDependencies.signalServiceAccountManager.getAciByUsername(Username(username)) - UsernameAciFetchResult.Success(aci) - } catch (e: UsernameIsNotAssociatedWithAnAccountException) { - Log.w(TAG, "[fetchAciFromUsername] Failed to get ACI for username hash", e) - UsernameAciFetchResult.NotFound - } catch (e: BaseUsernameException) { - Log.w(TAG, "[fetchAciFromUsername] Invalid username", e) - UsernameAciFetchResult.NotFound - } catch (e: IOException) { - Log.w(TAG, "[fetchAciFromUsername] Hit network error while trying to resolve ACI from username", e) + fun fetchAciForUsername(usernameString: String): UsernameAciFetchResult { + val username = try { + Username(usernameString) + } catch (e: BaseUsernameException) { + Log.w(TAG, "[fetchAciFromUsername] Invalid username", e) + return UsernameAciFetchResult.NotFound + } + + return when (val result = SignalNetwork.username.getAciByUsername(username)) { + is NetworkResult.Success -> UsernameAciFetchResult.Success(result.result) + is NetworkResult.StatusCodeError -> { + Log.w(TAG, "[fetchAciFromUsername] Failed to get ACI for username hash", result.exception) + when (result.code) { + 404 -> UsernameAciFetchResult.NotFound + else -> UsernameAciFetchResult.NetworkError + } + } + + is NetworkResult.NetworkError -> { + Log.w(TAG, "[fetchAciFromUsername] Hit network error while trying to resolve ACI from username", result.exception) UsernameAciFetchResult.NetworkError } + + is NetworkResult.ApplicationError -> throw result.throwable } } @@ -355,41 +373,56 @@ object UsernameRepository { @WorkerThread private fun reserveUsernameInternal(nickname: String, discriminator: String?): Result { - return try { - val candidates: List = if (discriminator == null) { + val candidates: List = try { + if (discriminator == null) { Username.candidatesFrom(nickname, UsernameUtil.MIN_NICKNAME_LENGTH, UsernameUtil.MAX_NICKNAME_LENGTH) } else { listOf(Username("$nickname${Usernames.DELIMITER}$discriminator")) } - - val hashes: List = candidates - .map { Base64.encodeUrlSafeWithoutPadding(it.hash) } - - val response = accountManager.reserveUsername(hashes) - - val hashIndex = hashes.indexOf(response.usernameHash) - if (hashIndex == -1) { - Log.w(TAG, "[reserveUsername] The response hash could not be found in our set of hashes.") - return failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR) - } - - Log.i(TAG, "[reserveUsername] Successfully reserved username.") - success(UsernameState.Reserved(candidates[hashIndex])) } catch (e: BaseUsernameException) { Log.w(TAG, "[reserveUsername] An error occurred while generating candidates.") - failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR) - } catch (e: UsernameTakenException) { - Log.w(TAG, "[reserveUsername] Username taken.") - failure(UsernameSetResult.USERNAME_UNAVAILABLE) - } catch (e: UsernameMalformedException) { - Log.w(TAG, "[reserveUsername] Username malformed.") - failure(UsernameSetResult.USERNAME_INVALID) - } catch (e: RateLimitException) { - Log.w(TAG, "[reserveUsername] Rate limit exceeded.") - failure(UsernameSetResult.RATE_LIMIT_ERROR) - } catch (e: IOException) { - Log.w(TAG, "[reserveUsername] Generic network exception.", e) - failure(UsernameSetResult.NETWORK_ERROR) + return failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR) + } + + val hashes: List = candidates + .map { Base64.encodeUrlSafeWithoutPadding(it.hash) } + + return when (val result = SignalNetwork.account.reserveUsername(hashes)) { + is NetworkResult.Success -> { + val hashIndex = hashes.indexOf(result.result.usernameHash) + if (hashIndex == -1) { + Log.w(TAG, "[reserveUsername] The response hash could not be found in our set of hashes.") + return failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR) + } + + Log.i(TAG, "[reserveUsername] Successfully reserved username.") + success(UsernameState.Reserved(candidates[hashIndex])) + } + is NetworkResult.StatusCodeError -> { + when (result.code) { + 409 -> { + Log.w(TAG, "[reserveUsername] Username taken.") + failure(UsernameSetResult.USERNAME_UNAVAILABLE) + } + 422 -> { + Log.w(TAG, "[reserveUsername] Username malformed.") + failure(UsernameSetResult.USERNAME_INVALID) + } + 429 -> { + Log.w(TAG, "[reserveUsername] Rate limit exceeded.") + failure(UsernameSetResult.RATE_LIMIT_ERROR) + } + else -> { + Log.w(TAG, "[reserveUsername] Generic network exception.", result.exception) + failure(UsernameSetResult.NETWORK_ERROR) + } + } + } + is NetworkResult.NetworkError -> { + Log.w(TAG, "[reserveUsername] Generic network exception.", result.exception) + failure(UsernameSetResult.NETWORK_ERROR) + } + is NetworkResult.ApplicationError -> throw result.throwable } } @@ -402,25 +435,27 @@ object UsernameRepository { return UsernameSetResult.NETWORK_ERROR } - return try { - val oldUsernameLink = SignalStore.account.usernameLink ?: return UsernameSetResult.USERNAME_INVALID - val newUsernameLink = updatedUsername.generateLink(oldUsernameLink.entropy) - val usernameLinkComponents = accountManager.updateUsernameLink(newUsernameLink) + val oldUsernameLink = SignalStore.account.usernameLink ?: return UsernameSetResult.USERNAME_INVALID + val newUsernameLink = updatedUsername.generateLink(oldUsernameLink.entropy) - SignalStore.account.username = updatedUsername.username - SignalStore.account.usernameLink = usernameLinkComponents - SignalDatabase.recipients.setUsername(Recipient.self().id, updatedUsername.username) - SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC - SignalStore.account.usernameSyncErrorCount = 0 + return when (val result = SignalNetwork.account.updateUsernameLink(newUsernameLink)) { + is NetworkResult.Success -> { + SignalStore.account.username = updatedUsername.username + SignalStore.account.usernameLink = result.result + SignalDatabase.recipients.setUsername(Recipient.self().id, updatedUsername.username) + SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC + SignalStore.account.usernameSyncErrorCount = 0 - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.") + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.") - UsernameSetResult.SUCCESS - } catch (e: IOException) { - Log.w(TAG, "[updateUsernameDisplayForCurrentLink] Generic network exception.", e) - UsernameSetResult.NETWORK_ERROR + UsernameSetResult.SUCCESS + } + else -> { + Log.w(TAG, "[updateUsernameDisplayForCurrentLink] Generic network exception.", result.getCause()) + UsernameSetResult.NETWORK_ERROR + } } } @@ -433,32 +468,55 @@ object UsernameRepository { return UsernameSetResult.NETWORK_ERROR } - return try { - val linkComponents: UsernameLinkComponents = accountManager.confirmUsernameAndCreateNewLink(username) + val link = username.generateLink() - SignalStore.account.username = username.username - SignalStore.account.usernameLink = linkComponents - SignalDatabase.recipients.setUsername(Recipient.self().id, username.username) - SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC - SignalStore.account.usernameSyncErrorCount = 0 + return when (val result = SignalNetwork.account.confirmUsername(username, link)) { + is NetworkResult.Success -> { + SignalStore.account.username = username.username + SignalStore.account.usernameLink = UsernameLinkComponents(link.entropy, result.result) + SignalDatabase.recipients.setUsername(Recipient.self().id, username.username) + SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC + SignalStore.account.usernameSyncErrorCount = 0 - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.") + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.") - UsernameSetResult.SUCCESS - } catch (e: UsernameTakenException) { - Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username gone.") - UsernameSetResult.USERNAME_UNAVAILABLE - } catch (e: UsernameIsNotReservedException) { - Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.") - UsernameSetResult.USERNAME_INVALID - } catch (e: BaseUsernameException) { - Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.") - UsernameSetResult.USERNAME_INVALID - } catch (e: IOException) { - Log.w(TAG, "[confirmUsernameAndCreateNewLink] Generic network exception.", e) - UsernameSetResult.NETWORK_ERROR + UsernameSetResult.SUCCESS + } + + is NetworkResult.StatusCodeError -> { + when (result.code) { + 409 -> { + Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.") + UsernameSetResult.USERNAME_INVALID + } + + 410 -> { + Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username gone.") + UsernameSetResult.USERNAME_UNAVAILABLE + } + + else -> { + Log.w(TAG, "[confirmUsernameAndCreateNewLink] Generic network exception.", result.exception) + UsernameSetResult.NETWORK_ERROR + } + } + } + + is NetworkResult.NetworkError -> { + Log.w(TAG, "[confirmUsernameAndCreateNewLink] Generic network exception.", result.exception) + UsernameSetResult.NETWORK_ERROR + } + + is NetworkResult.ApplicationError -> { + if (result.throwable is BaseUsernameException) { + Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.") + UsernameSetResult.USERNAME_INVALID + } else { + throw result.throwable + } + } } } @@ -469,43 +527,65 @@ object UsernameRepository { return UsernameDeleteResult.NETWORK_ERROR } - return try { - accountManager.deleteUsername() - SignalDatabase.recipients.setUsername(Recipient.self().id, null) - SignalStore.account.username = null - SignalStore.account.usernameLink = null - SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC - SignalStore.account.usernameSyncErrorCount = 0 - SignalDatabase.recipients.markNeedsSync(Recipient.self().id) - StorageSyncHelper.scheduleSyncForDataChange() - Log.i(TAG, "[deleteUsername] Successfully deleted the username.") - UsernameDeleteResult.SUCCESS - } catch (e: IOException) { - Log.w(TAG, "[deleteUsername] Generic network exception.", e) - UsernameDeleteResult.NETWORK_ERROR + return when (val result = SignalNetwork.account.deleteUsername()) { + is NetworkResult.Success -> { + SignalDatabase.recipients.setUsername(Recipient.self().id, null) + SignalStore.account.username = null + SignalStore.account.usernameLink = null + SignalStore.account.usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC + SignalStore.account.usernameSyncErrorCount = 0 + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + Log.i(TAG, "[deleteUsername] Successfully deleted the username.") + UsernameDeleteResult.SUCCESS + } + else -> { + Log.w(TAG, "[deleteUsername] Generic network exception.", result.getCause()) + UsernameDeleteResult.NETWORK_ERROR + } } } @WorkerThread @JvmStatic private fun reclaimUsernameIfNecessaryInternal(username: Username, usernameLinkComponents: UsernameLinkComponents): UsernameReclaimResult { - try { - accountManager.reclaimUsernameAndLink(username, usernameLinkComponents) - } catch (e: UsernameTakenException) { - Log.w(TAG, "[reclaimUsername] Username gone.") - return UsernameReclaimResult.PERMANENT_ERROR - } catch (e: UsernameIsNotReservedException) { - Log.w(TAG, "[reclaimUsername] Username was not reserved.") - return UsernameReclaimResult.PERMANENT_ERROR - } catch (e: BaseUsernameException) { - Log.w(TAG, "[reclaimUsername] Invalid username.") - return UsernameReclaimResult.PERMANENT_ERROR - } catch (e: IOException) { - Log.w(TAG, "[reclaimUsername] Network error.", e) - return UsernameReclaimResult.NETWORK_ERROR - } + val link = username.generateLink(usernameLinkComponents.entropy) - return UsernameReclaimResult.SUCCESS + return when (val result = SignalNetwork.account.confirmUsername(username, link)) { + is NetworkResult.Success -> UsernameReclaimResult.SUCCESS + is NetworkResult.StatusCodeError -> { + when (result.code) { + 409 -> { + Log.w(TAG, "[reclaimUsername] Username was not reserved.") + UsernameReclaimResult.PERMANENT_ERROR + } + + 410 -> { + Log.w(TAG, "[reclaimUsername] Username gone.") + UsernameReclaimResult.PERMANENT_ERROR + } + + else -> { + Log.w(TAG, "[reclaimUsername] Network error.", result.exception) + UsernameReclaimResult.NETWORK_ERROR + } + } + } + + is NetworkResult.NetworkError -> { + Log.w(TAG, "[reclaimUsername] Network error.", result.exception) + UsernameReclaimResult.NETWORK_ERROR + } + + is NetworkResult.ApplicationError -> { + if (result.throwable is BaseUsernameException) { + Log.w(TAG, "[reclaimUsername] Invalid username.") + UsernameReclaimResult.PERMANENT_ERROR + } else { + throw result.throwable + } + } + } } enum class UsernameSetResult { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt index a29ac52a5f..36f1456b85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/findby/FindByViewModel.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import org.signal.core.util.concurrent.safeBlockingGet import org.thoughtcrime.securesms.profiles.manage.UsernameRepository import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientRepository @@ -66,7 +65,7 @@ class FindByViewModel( return FindByResult.InvalidEntry } - return when (val result = UsernameRepository.fetchAciForUsername(username = username).safeBlockingGet()) { + return when (val result = UsernameRepository.fetchAciForUsername(usernameString = username)) { UsernameRepository.UsernameAciFetchResult.NetworkError -> FindByResult.NotFound() UsernameRepository.UsernameAciFetchResult.NotFound -> FindByResult.NotFound() is UsernameRepository.UsernameAciFetchResult.Success -> FindByResult.Success(Recipient.externalUsername(result.aci, username).id) diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt index 9341e7830d..2876deb136 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/app/account/export/ExportAccountDataTest.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.components.settings.app.account.export +import android.annotation.SuppressLint import android.app.Application import com.fasterxml.jackson.core.JsonParseException import io.mockk.every @@ -16,10 +17,11 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule import org.thoughtcrime.securesms.util.JsonUtils -import org.whispersystems.signalservice.api.SignalServiceAccountManager +import org.whispersystems.signalservice.api.NetworkResult import java.io.IOException @RunWith(RobolectricTestRunner::class) @@ -65,14 +67,14 @@ class ExportAccountDataTest { } """ + @SuppressLint("CheckResult") @Test fun `Export json without text field`() { val scheduler = TestScheduler() - val accountManager: SignalServiceAccountManager = mockk { - every { accountDataReport } returns mockJson - } - val mockRepository = ExportAccountDataRepository(accountManager) - val viewModel = ExportAccountDataViewModel(mockRepository) + + every { SignalNetwork.account.accountDataReport() } returns NetworkResult.Success(mockJson) + + val viewModel = ExportAccountDataViewModel(ExportAccountDataRepository()) viewModel.setExportAsTxt() viewModel.onGenerateReport() diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt index 8a448e45f4..e2ee4c50f1 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceDataStore import org.whispersystems.signalservice.api.SignalServiceMessageReceiver import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.account.AccountApi import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations @@ -46,6 +47,7 @@ import org.whispersystems.signalservice.api.services.CallLinksService import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.ProfileService import org.whispersystems.signalservice.api.storage.StorageServiceApi +import org.whispersystems.signalservice.api.username.UsernameApi import org.whispersystems.signalservice.api.websocket.SignalWebSocket import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration import org.whispersystems.signalservice.internal.push.PushServiceSocket @@ -60,7 +62,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider { return mockk(relaxed = true) } - override fun provideSignalServiceAccountManager(pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager { + override fun provideSignalServiceAccountManager(authWebSocket: AccountApi, pushServiceSocket: PushServiceSocket, groupsV2Operations: GroupsV2Operations): SignalServiceAccountManager { return mockk(relaxed = true) } @@ -245,4 +247,12 @@ class MockApplicationDependencyProvider : AppDependencies.Provider { override fun provideUnauthWebSocket(signalServiceConfigurationSupplier: Supplier, libSignalNetworkSupplier: Supplier): SignalWebSocket.UnauthenticatedWebSocket { return mockk(relaxed = true) } + + override fun provideAccountApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): AccountApi { + return mockk(relaxed = true) + } + + override fun provideUsernameApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): UsernameApi { + return mockk(relaxed = true) + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt index 99046ddb17..0837975f50 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api import org.signal.core.util.concurrent.safeBlockingGet import org.whispersystems.signalservice.api.NetworkResult.StatusCodeError import org.whispersystems.signalservice.api.NetworkResult.Success +import org.whispersystems.signalservice.api.push.exceptions.MalformedRequestException import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.websocket.SignalWebSocket @@ -159,6 +160,20 @@ sealed class NetworkResult( /** Indicates we got a response, but it was a non-2xx response. */ data class StatusCodeError(val code: Int, val stringBody: String?, val binaryBody: ByteArray?, val exception: NonSuccessfulResponseCodeException) : NetworkResult() { constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.stringBody, e.binaryBody, e) + + inline fun parseJsonBody(): T? { + return try { + if (stringBody != null) { + JsonUtil.fromJsonResponse(stringBody, T::class.java) + } else if (binaryBody != null) { + JsonUtil.fromJsonResponse(binaryBody, T::class.java) + } else { + null + } + } catch (e: MalformedRequestException) { + null + } + } } /** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */ @@ -358,8 +373,9 @@ private fun WebsocketResponse.toStatusCodeError(): NetworkResult { } private fun WebsocketResponse.toSuccess(responseJsonClass: KClass): NetworkResult { - if (responseJsonClass == Unit::class) { - return Success(responseJsonClass.cast(Unit)) + return when (responseJsonClass) { + Unit::class -> Success(responseJsonClass.cast(Unit)) + String::class -> Success(responseJsonClass.cast(this.body)) + else -> Success(JsonUtil.fromJson(this.body, responseJsonClass.java)) } - return Success(JsonUtil.fromJson(this.body, responseJsonClass.java)) } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt new file mode 100644 index 0000000000..6c16b1fbd9 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api + +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import java.io.IOException + +/** + * Bridge layer to convert [NetworkResult]s into the response data or thrown exceptions. + */ +object NetworkResultUtil { + + /** + * Convert to a basic [IOException] or [NonSuccessfulResponseCodeException]. Should only be used when you don't + * need a specific flavor of IOException for a specific response code. + */ + @JvmStatic + @Throws(IOException::class) + fun toBasicLegacy(result: NetworkResult): T { + return when (result) { + is NetworkResult.Success -> result.result + is NetworkResult.ApplicationError -> { + throw when (val error = result.throwable) { + is IOException, is RuntimeException -> error + else -> RuntimeException(error) + } + } + is NetworkResult.NetworkError -> throw result.exception + is NetworkResult.StatusCodeError -> { + when (result.code) { + 401, 403 -> throw AuthorizationFailedException(result.code, "Authorization failed!") + else -> throw result.exception + } + } + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 48b9e60b0a..7853644b53 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -6,17 +6,10 @@ package org.whispersystems.signalservice.api; -import org.signal.core.util.Base64; import org.signal.libsignal.net.Network; -import org.signal.libsignal.protocol.IdentityKeyPair; -import org.signal.libsignal.protocol.InvalidKeyException; -import org.signal.libsignal.protocol.ecc.ECPublicKey; -import org.signal.libsignal.usernames.BaseUsernameException; -import org.signal.libsignal.usernames.Username; -import org.signal.libsignal.usernames.Username.UsernameLink; 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.AccountApi; import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; @@ -24,9 +17,7 @@ import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; -import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.payments.CurrencyConversions; import org.whispersystems.signalservice.api.profiles.AvatarUploadParams; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -35,28 +26,21 @@ import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; -import org.whispersystems.signalservice.api.push.UsernameLinkComponents; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.registration.RegistrationApi; import org.whispersystems.signalservice.api.services.CdsiV2Service; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV2; import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV3; -import org.whispersystems.signalservice.api.util.CredentialsProvider; -import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher; import org.whispersystems.signalservice.internal.push.AuthCredentials; import org.whispersystems.signalservice.internal.push.CdsiAuthResponse; import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts; import org.whispersystems.signalservice.internal.push.PaymentAddress; import org.whispersystems.signalservice.internal.push.ProfileAvatarData; -import org.whispersystems.signalservice.internal.push.ProvisionMessage; -import org.whispersystems.signalservice.internal.push.ProvisioningVersion; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.RemoteConfigResponse; -import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse; import org.whispersystems.signalservice.internal.push.WhoAmIResponse; import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; @@ -69,7 +53,6 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -78,7 +61,6 @@ import java.util.function.Consumer; import javax.annotation.Nonnull; import io.reactivex.rxjava3.core.Single; -import okio.ByteString; /** * The main interface for creating, registering, and @@ -91,9 +73,9 @@ public class SignalServiceAccountManager { private static final String TAG = SignalServiceAccountManager.class.getSimpleName(); private final PushServiceSocket pushServiceSocket; - private final CredentialsProvider credentials; private final GroupsV2Operations groupsV2Operations; private final SignalServiceConfiguration configuration; + private final AccountApi accountApi; /** * Construct a SignalServiceAccountManager. @@ -118,15 +100,16 @@ public class SignalServiceAccountManager { GroupsV2Operations gv2Operations = new GroupsV2Operations(ClientZkOperations.create(configuration), maxGroupSize); return new SignalServiceAccountManager( + null, new PushServiceSocket(configuration, credentialProvider, signalAgent, gv2Operations.getProfileOperations(), automaticNetworkRetry), gv2Operations ); } - public SignalServiceAccountManager(PushServiceSocket pushServiceSocket, GroupsV2Operations groupsV2Operations) { + public SignalServiceAccountManager(AccountApi accountApi, PushServiceSocket pushServiceSocket, GroupsV2Operations groupsV2Operations) { + this.accountApi = accountApi; this.groupsV2Operations = groupsV2Operations; this.pushServiceSocket = pushServiceSocket; - this.credentials = pushServiceSocket.getCredentialsProvider(); this.configuration = pushServiceSocket.getConfiguration(); } @@ -147,21 +130,7 @@ public class SignalServiceAccountManager { } public WhoAmIResponse getWhoAmI() throws IOException { - return this.pushServiceSocket.getWhoAmI(); - } - - /** - * Register/Unregister a Google Cloud Messaging registration ID. - * - * @param gcmRegistrationId The GCM id to register. A call with an absent value will unregister. - * @throws IOException - */ - public void setGcmId(Optional gcmRegistrationId) throws IOException { - if (gcmRegistrationId.isPresent()) { - this.pushServiceSocket.registerGcmId(gcmRegistrationId.get()); - } else { - this.pushServiceSocket.unregisterGcmId(); - } + return NetworkResultUtil.toBasicLegacy(accountApi.whoAmI()); } /** @@ -176,17 +145,6 @@ public class SignalServiceAccountManager { pushServiceSocket.requestPushChallenge(sessionId, gcmRegistrationId); } - /** - * Refresh account attributes with server. - * - * @throws IOException - */ - public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes) - throws IOException - { - this.pushServiceSocket.setAccountAttributes(accountAttributes); - } - /** * Register an identity key, signed prekey, and list of one time prekeys * with the server. @@ -207,13 +165,6 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getAvailablePreKeys(serviceIdType); } - /** - * @return True if the identifier corresponds to a registered user, otherwise false. - */ - public boolean isIdentifierRegistered(ServiceId identifier) throws IOException { - return pushServiceSocket.isIdentifierRegistered(identifier); - } - @SuppressWarnings("SameParameterValue") public CdsiV2Service.Response getRegisteredUsersWithCdsi(Set previousE164s, Set newE164s, @@ -268,20 +219,6 @@ public class SignalServiceAccountManager { } } - /** - * Enables registration lock for this account. - */ - public void enableRegistrationLock(MasterKey masterKey) throws IOException { - pushServiceSocket.setRegistrationLockV2(masterKey.deriveRegistrationLock()); - } - - /** - * Disables registration lock for this account. - */ - public void disableRegistrationLock() throws IOException { - pushServiceSocket.disableRegistrationLockV2(); - } - public RemoteConfigResult getRemoteConfig() throws IOException { RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig(); Map out = new HashMap<>(); @@ -293,10 +230,6 @@ public class SignalServiceAccountManager { return new RemoteConfigResult(out, response.getServerEpochTime()); } - public String getAccountDataReport() throws IOException { - return pushServiceSocket.getAccountDataReport(); - } - public List getTurnServerInfo() throws IOException { List relays = this.pushServiceSocket.getCallingRelays().getRelays(); return relays != null ? relays : Collections.emptyList(); @@ -377,69 +310,6 @@ public class SignalServiceAccountManager { } } - public ACI getAciByUsername(Username username) throws IOException { - return this.pushServiceSocket.getAciByUsernameHash(Base64.encodeUrlSafeWithoutPadding(username.getHash())); - } - - public ReserveUsernameResponse reserveUsername(List usernameHashes) throws IOException { - return this.pushServiceSocket.reserveUsername(usernameHashes); - } - - public UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException { - try { - UsernameLink link = username.generateLink(); - UUID serverId = this.pushServiceSocket.confirmUsernameAndCreateNewLink(username, link); - - return new UsernameLinkComponents(link.getEntropy(), serverId); - } catch (BaseUsernameException e) { - throw new AssertionError(e); - } - } - - public UsernameLinkComponents reclaimUsernameAndLink(Username username, UsernameLinkComponents linkComponents) throws IOException { - try { - UsernameLink link = username.generateLink(linkComponents.getEntropy()); - UUID serverId = this.pushServiceSocket.confirmUsernameAndCreateNewLink(username, link); - - return new UsernameLinkComponents(link.getEntropy(), serverId); - } catch (BaseUsernameException e) { - throw new AssertionError(e); - } - } - - public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException { - UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithoutPadding(newUsernameLink.getEncryptedUsername()), true); - - return new UsernameLinkComponents(newUsernameLink.getEntropy(), serverId); - } - - public void deleteUsername() throws IOException { - this.pushServiceSocket.deleteUsername(); - } - - public UsernameLinkComponents createUsernameLink(Username username) throws IOException { - try { - UsernameLink link = username.generateLink(); - UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithPadding(link.getEncryptedUsername()), false); - - return new UsernameLinkComponents(link.getEntropy(), serverId); - } catch (BaseUsernameException e) { - throw new AssertionError(e); - } - } - - public void deleteUsernameLink() throws IOException { - this.pushServiceSocket.deleteUsernameLink(); - } - - public byte[] getEncryptedUsernameFromLinkServerId(UUID serverId) throws IOException { - return this.pushServiceSocket.getEncryptedUsernameFromLinkServerId(serverId); - } - - public void deleteAccount() throws IOException { - this.pushServiceSocket.deleteAccount(); - } - public void requestRateLimitPushChallenge() throws IOException { this.pushServiceSocket.requestRateLimitPushChallenge(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index c6b47af941..30c1596569 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -2718,7 +2718,8 @@ public class SignalServiceMessageSender { } } - private void handleMismatchedDevices(PushServiceSocket socket, SignalServiceAddress recipient, + private void handleMismatchedDevices(PushServiceSocket socket, + SignalServiceAddress recipient, MismatchedDevices mismatchedDevices) throws IOException, UntrustedIdentityException { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt new file mode 100644 index 0000000000..4992a0f036 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt @@ -0,0 +1,243 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.account + +import org.signal.core.util.Base64 +import org.signal.core.util.Base64.encodeUrlSafeWithoutPadding +import org.signal.libsignal.usernames.BaseUsernameException +import org.signal.libsignal.usernames.Username +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.UsernameLinkComponents +import org.whispersystems.signalservice.api.websocket.SignalWebSocket +import org.whispersystems.signalservice.internal.delete +import org.whispersystems.signalservice.internal.get +import org.whispersystems.signalservice.internal.push.ConfirmUsernameRequest +import org.whispersystems.signalservice.internal.push.ConfirmUsernameResponse +import org.whispersystems.signalservice.internal.push.GcmRegistrationId +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import org.whispersystems.signalservice.internal.push.ReserveUsernameRequest +import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse +import org.whispersystems.signalservice.internal.push.SetUsernameLinkRequestBody +import org.whispersystems.signalservice.internal.push.SetUsernameLinkResponseBody +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse +import org.whispersystems.signalservice.internal.push.WhoAmIResponse +import org.whispersystems.signalservice.internal.put +import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage +import java.security.SecureRandom +import java.util.UUID + +/** + * Various user account specific APIs to get, update, and delete account data. + */ +class AccountApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket) { + + private val random = SecureRandom() + + /** + * Fetch information about yourself. + * + * GET /v1/accounts/whoami + * - 200: Success + */ + fun whoAmI(): NetworkResult { + val request = WebSocketRequestMessage.get("/v1/accounts/whoami") + return NetworkResult.fromWebSocketRequest(authWebSocket, request, WhoAmIResponse::class) + } + + /** + * PUT /v1/accounts/gcm + * - 200: Success + */ + fun setFcmToken(fcmToken: String): NetworkResult { + val request = WebSocketRequestMessage.put("/v1/accounts/gcm", GcmRegistrationId(fcmToken, true)) + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * DELETE /v1/account/gcm + * - 204: Success + */ + fun clearFcmToken(): NetworkResult { + val request = WebSocketRequestMessage.delete("/v1/accounts/gcm") + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * Set account attributes. + * + * PUT /v1/accounts/attributes + * - 200: Success + */ + fun setAccountAttributes(accountAttributes: AccountAttributes): NetworkResult { + val request = WebSocketRequestMessage.put("/v1/accounts/attributes", accountAttributes) + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * PUT /v1/accounts/registration_lock + * - 200: Success + */ + fun enableRegistrationLock(registrationLock: String): NetworkResult { + val request = WebSocketRequestMessage.put("/v1/accounts/registration_lock", PushServiceSocket.RegistrationLockV2(registrationLock)) + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * DELETE /v1/accounts/registration_lock + * - 204: Success + */ + fun disableRegistrationLock(): NetworkResult { + val request = WebSocketRequestMessage.delete("/v1/accounts/registration_lock") + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * DELETE /v1/accounts/me + * - 204: Success + */ + fun deleteAccount(): NetworkResult { + val request = WebSocketRequestMessage.delete("/v1/accounts/me") + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * Generate and get an account data report. + * + * GET /v2/accounts/data_report + * - 200: Success + */ + fun accountDataReport(): NetworkResult { + val request = WebSocketRequestMessage.get("/v2/accounts/data_report") + return NetworkResult.fromWebSocketRequest(authWebSocket, request, String::class) + } + + /** + * Changes the phone number that an account is associated with. + * + * PUT /v2/accounts/number + * - 200: Success + * - 403: No recovery password provided + * - 409: Mismatched device ids to notify + * - 410: Mismatched device registration ids to notify + * - 422: Unable to parse [ChangePhoneNumberRequest] + * - 423: Account reglock enabled for new phone number + * - 429: Rate limited + */ + fun changeNumber(changePhoneNumberRequest: ChangePhoneNumberRequest): NetworkResult { + val request = WebSocketRequestMessage.put("/v2/accounts/number", changePhoneNumberRequest) + return NetworkResult.fromWebSocketRequest(authWebSocket, request, VerifyAccountResponse::class) + } + + /** + * Distributes key material to linked devices after an account becomes fully PNP capable. + * + * PUT /v2/accounts/phone_number_identity_key_distribution + * - 200: Success + * - 401: Unauthorized + * - 403: Called from non-primary device + * - 409: Mismatched devices + * - 410: Registration ids do not match + * - 422: Request is malformed + */ + fun distributePniKeys(distributionRequest: PniKeyDistributionRequest): NetworkResult { + val request = WebSocketRequestMessage.put("/v2/accounts/phone_number_identity_key_distribution", distributionRequest) + return NetworkResult.fromWebSocketRequest(authWebSocket, request, VerifyAccountResponse::class) + } + + /** + * Reserve a username for the account. This replaces an existing reservation if one exists. The username is guaranteed to be available for 5 minutes and can + * be confirmed with confirmUsername. + * + * PUT /v1/accounts/username_hash/reserve + * - 200: Success + * - 409: Username taken + * - 422: Username malformed + * - 429: Rate limited + * + * @param usernameHashes A list of hashed usernames encoded as web-safe base64 strings without padding. The list will have a max length of 20, and each hash will be 32 bytes. + * @return The reserved username. It is available for confirmation for 5 minutes. + */ + fun reserveUsername(usernameHashes: List): NetworkResult { + val request = WebSocketRequestMessage.put("/v1/accounts/username_hash/reserve", ReserveUsernameRequest(usernameHashes)) + return NetworkResult.fromWebSocketRequest(authWebSocket, request, ReserveUsernameResponse::class) + } + + /** + * Set a previously reserved username for the account. + * + * PUT /v1/accounts/username_hash/confirm + * - 200: Success + * - 409: Username is not reserved + * - 410: Username unavailable + * - 422: Unable to parse [ConfirmUsernameRequest] + * - 429: Rate limited + * + * @param username The username the user wishes to confirm. + */ + fun confirmUsername(username: Username, link: Username.UsernameLink): NetworkResult { + val randomness = ByteArray(32) + random.nextBytes(randomness) + + val proof: ByteArray = try { + username.generateProofWithRandomness(randomness) + } catch (e: BaseUsernameException) { + return NetworkResult.ApplicationError(e) + } + + val confirmUsernameRequest = ConfirmUsernameRequest( + encodeUrlSafeWithoutPadding(username.hash), + encodeUrlSafeWithoutPadding(proof), + encodeUrlSafeWithoutPadding(link.encryptedUsername) + ) + + val request = WebSocketRequestMessage.put("/v1/accounts/username_hash/confirm", confirmUsernameRequest) + return NetworkResult.fromWebSocketRequest(authWebSocket, request, ConfirmUsernameResponse::class) + .map { it.usernameLinkHandle } + } + + /** + * DELETE /v1/accounts/username_hash + * - 204: Success + */ + fun deleteUsername(): NetworkResult { + val request = WebSocketRequestMessage.delete("/v1/accounts/username_hash") + return NetworkResult.fromWebSocketRequest(authWebSocket, request) + } + + /** + * Creates a new username link for the given [usernameLink]. + * + * PUT /v1/accounts/username_link + * - 200: Success + * - 409: Username is not set + * - 422: Invalid [SetUsernameLinkRequestBody] format + * - 429: Rate limited + */ + fun createUsernameLink(usernameLink: Username.UsernameLink): NetworkResult { + return modifyUsernameLink(usernameLink, false) + } + + /** + * Update account username link for the given [usernameLink]. + * + * PUT /v1/accounts/username_link + * - 200: Success + * - 409: Username is not set + * - 422: Invalid [SetUsernameLinkRequestBody] format + * - 429: Rate limited + */ + fun updateUsernameLink(usernameLink: Username.UsernameLink): NetworkResult { + return modifyUsernameLink(usernameLink, true) + } + + private fun modifyUsernameLink(usernameLink: Username.UsernameLink, keepLinkHandle: Boolean): NetworkResult { + val encryptedUsername = Base64.encodeUrlSafeWithPadding(usernameLink.encryptedUsername) + val request = WebSocketRequestMessage.put("/v1/accounts/username_link", SetUsernameLinkRequestBody(encryptedUsername, keepLinkHandle)) + + return NetworkResult.fromWebSocketRequest(authWebSocket, request, SetUsernameLinkResponseBody::class) + .map { UsernameLinkComponents(usernameLink.entropy, it.usernameLinkHandle) } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt index 10656ee270..85bdca2357 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/registration/RegistrationApi.kt @@ -9,8 +9,6 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey import org.signal.registration.proto.RegistrationProvisionMessage import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.account.AccountAttributes -import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest -import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest import org.whispersystems.signalservice.api.account.PreKeyCollection import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher import org.whispersystems.signalservice.internal.push.BackupV2AuthCheckResponse @@ -129,23 +127,6 @@ class RegistrationApi( } } - /** - * Changes the phone number that an account is associated with. - * - * `PUT /v2/accounts/number` - */ - fun changeNumber(requestBody: ChangePhoneNumberRequest): NetworkResult { - return NetworkResult.fromFetch { - pushServiceSocket.changeNumber(requestBody) - } - } - - fun distributePniKeys(requestBody: PniKeyDistributionRequest): NetworkResult { - return NetworkResult.fromFetch { - pushServiceSocket.distributePniKeys(requestBody) - } - } - /** * Encrypts and sends the [RegistrationProvisionMessage] from the current primary (old device) to the new device over * the provisioning web socket identified by [deviceIdentifier]. diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt new file mode 100644 index 0000000000..1d0db71dfe --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.username + +import org.signal.core.util.Base64 +import org.signal.libsignal.usernames.Username +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.account.AccountApi +import org.whispersystems.signalservice.api.push.ServiceId +import org.whispersystems.signalservice.api.websocket.SignalWebSocket +import org.whispersystems.signalservice.internal.get +import org.whispersystems.signalservice.internal.push.GetAciByUsernameResponse +import org.whispersystems.signalservice.internal.push.GetUsernameFromLinkResponseBody +import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage +import java.util.UUID + +/** + * Username specific APIs related to learning service information for someone else by username. + * For APIs to manage your own username, see [AccountApi]. + */ +class UsernameApi(private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket) { + + /** + * Gets the ACI for the given [username], if it exists. This is an unauthenticated request. + * + * GET /v1/accounts/username_hash/[Username.getHash] + * - 200: Success + * - 400: Request must not be authenticated + * - 404: Hash is not associated with an account + */ + fun getAciByUsername(username: Username): NetworkResult { + val usernameHash = Base64.encodeUrlSafeWithoutPadding(username.hash) + val request = WebSocketRequestMessage.get("/v1/accounts/username_hash/$usernameHash") + + return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, GetAciByUsernameResponse::class) + .map { ServiceId.ACI.from(UUID.fromString(it.uuid)) } + } + + /** + * Given a link serverId, this will return the encrypted username associated with the link. + * + * GET /v1/accounts/username_hash/[serverId] + * - 200: Success + * - 400: Request must not be authenticated + * - 404: Username link not found for server id + * - 422: Invalid request format + * - 429: Rate limited + */ + fun getEncryptedUsernameFromLinkServerId(serverId: UUID): NetworkResult { + val request = WebSocketRequestMessage.get("/v1/accounts/username_link/$serverId") + return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, GetUsernameFromLinkResponseBody::class) + .map { Base64.decode(it.usernameLinkEncryptedValue) } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ConfirmUsernameRequest.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ConfirmUsernameRequest.java index 162ce6b6af..1b7282812c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ConfirmUsernameRequest.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ConfirmUsernameRequest.java @@ -2,7 +2,7 @@ package org.whispersystems.signalservice.internal.push; import com.fasterxml.jackson.annotation.JsonProperty; -class ConfirmUsernameRequest { +public class ConfirmUsernameRequest { @JsonProperty private String usernameHash; @@ -12,7 +12,7 @@ class ConfirmUsernameRequest { @JsonProperty private String encryptedUsername; - ConfirmUsernameRequest(String usernameHash, String zkProof, String encryptedUsername) { + public ConfirmUsernameRequest(String usernameHash, String zkProof, String encryptedUsername) { this.usernameHash = usernameHash; this.zkProof = zkProof; this.encryptedUsername = encryptedUsername; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/GetAciByUsernameResponse.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/GetAciByUsernameResponse.java index 396e8d1b12..2eec89344f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/GetAciByUsernameResponse.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/GetAciByUsernameResponse.java @@ -6,13 +6,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; * JSON POJO that represents the returned ACI from a call to * /v1/account/username/[username] */ -class GetAciByUsernameResponse { +public class GetAciByUsernameResponse { @JsonProperty private String uuid; - GetAciByUsernameResponse() {} + public GetAciByUsernameResponse() {} - String getUuid() { + public String getUuid() { return uuid; } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index fb07ed10b9..41e13848db 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -21,8 +21,6 @@ import org.signal.libsignal.protocol.kem.KEMPublicKey; import org.signal.libsignal.protocol.logging.Log; import org.signal.libsignal.protocol.state.PreKeyBundle; import org.signal.libsignal.protocol.util.Pair; -import org.signal.libsignal.usernames.BaseUsernameException; -import org.signal.libsignal.usernames.Username; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.calllinks.CreateCallLinkCredentialRequest; @@ -46,8 +44,6 @@ import org.signal.storageservice.protos.groups.GroupJoinInfo; import org.signal.storageservice.protos.groups.GroupResponse; import org.signal.storageservice.protos.groups.Member; import org.whispersystems.signalservice.api.account.AccountAttributes; -import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest; -import org.whispersystems.signalservice.api.account.PniKeyDistributionRequest; import org.whispersystems.signalservice.api.account.PreKeyCollection; import org.whispersystems.signalservice.api.account.PreKeyUpload; import org.whispersystems.signalservice.api.archive.ArchiveCredentialPresentation; @@ -65,14 +61,9 @@ import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResp import org.whispersystems.signalservice.api.crypto.SealedSenderAccess; import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; -import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse; -import org.whispersystems.signalservice.api.link.SetDeviceNameRequest; -import org.whispersystems.signalservice.api.link.SetLinkedDeviceTransferArchiveRequest; -import org.whispersystems.signalservice.api.link.WaitForLinkedDeviceResponse; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.calls.CallingResponse; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.payments.CurrencyConversions; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; @@ -112,10 +103,6 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept import org.whispersystems.signalservice.api.push.exceptions.SubmitVerificationCodeRateLimitException; import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssociatedWithAnAccountException; -import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException; -import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException; -import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException; import org.whispersystems.signalservice.api.registration.RestoreMethodBody; import org.whispersystems.signalservice.api.storage.StorageAuthResponse; import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; @@ -123,7 +110,6 @@ import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIn import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse; import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse; import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret; -import org.whispersystems.signalservice.api.svr.SetShareSetRequest; import org.whispersystems.signalservice.api.svr.Svr3Credentials; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.Tls12SocketFactory; @@ -190,7 +176,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -201,7 +186,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import io.reactivex.rxjava3.annotations.NonNull; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; import okhttp3.Call; @@ -228,24 +212,6 @@ public class PushServiceSocket { private static final String TAG = PushServiceSocket.class.getSimpleName(); - private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/"; - private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/"; - private static final String PIN_PATH = "/v1/accounts/pin/"; - private static final String REGISTRATION_LOCK_PATH = "/v1/accounts/registration_lock"; - private static final String WHO_AM_I = "/v1/accounts/whoami"; - private static final String GET_USERNAME_PATH = "/v1/accounts/username_hash/%s"; - private static final String MODIFY_USERNAME_PATH = "/v1/accounts/username_hash"; - private static final String RESERVE_USERNAME_PATH = "/v1/accounts/username_hash/reserve"; - private static final String CONFIRM_USERNAME_PATH = "/v1/accounts/username_hash/confirm"; - private static final String USERNAME_LINK_PATH = "/v1/accounts/username_link"; - private static final String USERNAME_FROM_LINK_PATH = "/v1/accounts/username_link/%s"; - private static final String DELETE_ACCOUNT_PATH = "/v1/accounts/me"; - private static final String SET_DEVICE_NAME_PATH = "/v1/accounts/name?deviceId=%s"; - private static final String CHANGE_NUMBER_PATH = "/v2/accounts/number"; - private static final String IDENTIFIER_REGISTERED_PATH = "/v1/accounts/account/%s"; - private static final String REQUEST_ACCOUNT_DATA_PATH = "/v2/accounts/data_report"; - private static final String PNI_KEY_DISTRUBTION_PATH = "/v2/accounts/phone_number_identity_key_distribution"; - private static final String PREKEY_METADATA_PATH = "/v2/keys?identity=%s"; private static final String PREKEY_PATH = "/v2/keys?identity=%s"; private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s"; @@ -500,23 +466,6 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, VerifyAccountResponse.class); } - public WhoAmIResponse getWhoAmI() throws IOException { - return JsonUtil.fromJson(makeServiceRequest(WHO_AM_I, "GET", null), WhoAmIResponse.class); - } - - public boolean isIdentifierRegistered(ServiceId identifier) throws IOException { - try { - makeServiceRequestWithoutAuthentication(String.format(IDENTIFIER_REGISTERED_PATH, identifier.toString()), "HEAD", null); - return true; - } catch (NotFoundException e) { - return false; - } - } - - public String getAccountDataReport() throws IOException { - return makeServiceRequest(REQUEST_ACCOUNT_DATA_PATH, "GET", null); - } - public CdsiAuthResponse getCdsiAuth() throws IOException { String body = makeServiceRequest(CDSI_AUTH, "GET", null); return JsonUtil.fromJsonResponse(body, CdsiAuthResponse.class); @@ -667,33 +616,6 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, GetArchiveCdnCredentialsResponse.class); } - public void setShareSet(byte[] shareSet) throws IOException { - SetShareSetRequest request = new SetShareSetRequest(shareSet); - makeServiceRequest(SET_SHARE_SET_PATH, "PUT", JsonUtil.toJson(request)); - } - - public VerifyAccountResponse changeNumber(@Nonnull ChangePhoneNumberRequest changePhoneNumberRequest) - throws IOException - { - String requestBody = JsonUtil.toJson(changePhoneNumberRequest); - String responseBody = makeServiceRequest(CHANGE_NUMBER_PATH, "PUT", requestBody); - - return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class); - } - - public VerifyAccountResponse distributePniKeys(@NonNull PniKeyDistributionRequest distributionRequest) throws IOException { - String request = JsonUtil.toJson(distributionRequest); - String response = makeServiceRequest(PNI_KEY_DISTRUBTION_PATH, "PUT", request); - - return JsonUtil.fromJson(response, VerifyAccountResponse.class); - } - - public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes) - throws IOException - { - makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); - } - public void setRestoreMethodChosen(@Nonnull String token, @Nonnull RestoreMethodBody request) throws IOException { String body = JsonUtil.toJson(request); makeServiceRequest(String.format(Locale.US, SET_RESTORE_METHOD_PATH, urlEncode(token)), "PUT", body, NO_HEADERS, UNOPINIONATED_HANDLER, SealedSenderAccess.NONE); @@ -712,33 +634,10 @@ public class PushServiceSocket { JsonUtil.toJson(new ProvisioningMessage(Base64.encodeWithPadding(body)))); } - public void registerGcmId(@Nonnull String gcmRegistrationId) throws IOException { - GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId, true); - makeServiceRequest(REGISTER_GCM_PATH, "PUT", JsonUtil.toJson(registration)); - } - - public void unregisterGcmId() throws IOException { - makeServiceRequest(REGISTER_GCM_PATH, "DELETE", null); - } - public void requestPushChallenge(String sessionId, String gcmRegistrationId) throws IOException { patchVerificationSession(sessionId, gcmRegistrationId, null, null, null, null); } - /** Note: Setting a KBS Pin will clear this */ - public void removeRegistrationLockV1() throws IOException { - makeServiceRequest(PIN_PATH, "DELETE", null); - } - - public void setRegistrationLockV2(String registrationLock) throws IOException { - RegistrationLockV2 accountLock = new RegistrationLockV2(registrationLock); - makeServiceRequest(REGISTRATION_LOCK_PATH, "PUT", JsonUtil.toJson(accountLock)); - } - - public void disableRegistrationLockV2() throws IOException { - makeServiceRequest(REGISTRATION_LOCK_PATH, "DELETE", null); - } - public byte[] getSenderCertificate() throws IOException { String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null); return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate(); @@ -1226,136 +1125,6 @@ public class PushServiceSocket { return JsonUtil.fromJson(response, BackupV3AuthCheckResponse.class); } - /** - * GET /v1/accounts/username_hash/{usernameHash} - * - * Gets the ACI for the given username hash, if it exists. This is an unauthenticated request. - * - * This network request can have the following error responses: - *
    - *
  • 404 - The username given is not associated with an account
  • - *
  • 428 - Rate-limited, retry is available in the Retry-After header
  • - *
  • 400 - Bad Request. The request included authentication.
  • - *
- * - * @param usernameHash The usernameHash to look up. - * @return The ACI for the given username if it exists. - * @throws IOException if a network exception occurs. - */ - public @NonNull ACI getAciByUsernameHash(String usernameHash) throws IOException { - String response = makeServiceRequestWithoutAuthentication( - String.format(GET_USERNAME_PATH, urlEncode(usernameHash)), - "GET", - null, - NO_HEADERS, - (responseCode, body, getHeader) -> { - if (responseCode == 404) { - throw new UsernameIsNotAssociatedWithAnAccountException(); - } - } - ); - - GetAciByUsernameResponse getAciByUsernameResponse = JsonUtil.fromJsonResponse(response, GetAciByUsernameResponse.class); - return ACI.from(UUID.fromString(getAciByUsernameResponse.getUuid())); - } - - /** - * PUT /v1/accounts/username_hash/reserve - * Reserve a username for the account. This replaces an existing reservation if one exists. The username is guaranteed to be available for 5 minutes and can - * be confirmed with confirmUsername. - * - * @param usernameHashes A list of hashed usernames encoded as web-safe base64 strings without padding. The list will have a max length of 20, and each hash will be 32 bytes. - * @return The reserved username. It is available for confirmation for 5 minutes. - * @throws IOException Thrown when the username is invalid or taken, or when another network error occurs. - */ - public @NonNull ReserveUsernameResponse reserveUsername(@NonNull List usernameHashes) throws IOException { - ReserveUsernameRequest reserveUsernameRequest = new ReserveUsernameRequest(usernameHashes); - - String responseString = makeServiceRequest(RESERVE_USERNAME_PATH, "PUT", JsonUtil.toJson(reserveUsernameRequest), NO_HEADERS, (responseCode, body, getHeader) -> { - switch (responseCode) { - case 422: throw new UsernameMalformedException(); - case 409: throw new UsernameTakenException(); - } - }, SealedSenderAccess.NONE); - - return JsonUtil.fromJsonResponse(responseString, ReserveUsernameResponse.class); - } - - /** - * PUT /v1/accounts/username_hash/confirm - * Set a previously reserved username for the account. - * - * @param username The username the user wishes to confirm. - * @throws IOException Thrown when the username is invalid or taken, or when another network error occurs. - */ - public UUID confirmUsernameAndCreateNewLink(Username username, Username.UsernameLink link) throws IOException { - try { - byte[] randomness = new byte[32]; - random.nextBytes(randomness); - - byte[] proof = username.generateProofWithRandomness(randomness); - ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest( - Base64.encodeUrlSafeWithoutPadding(username.getHash()), - Base64.encodeUrlSafeWithoutPadding(proof), - Base64.encodeUrlSafeWithoutPadding(link.getEncryptedUsername()) - ); - - String response = makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body, getHeader) -> { - switch (responseCode) { - case 409: - throw new UsernameIsNotReservedException(); - case 410: - throw new UsernameTakenException(); - } - }, SealedSenderAccess.NONE); - - return JsonUtil.fromJson(response, ConfirmUsernameResponse.class).getUsernameLinkHandle(); - } catch (BaseUsernameException e) { - throw new IOException(e); - } - } - - /** - * Remove the username associated with the account. - */ - public void deleteUsername() throws IOException { - makeServiceRequest(MODIFY_USERNAME_PATH, "DELETE", null); - } - - /** - * Creates a new username link for a given username. - * @param encryptedUsername URL-safe base64-encoded encrypted username - * @return The serverId for the generated link. - */ - public UUID createUsernameLink(String encryptedUsername, boolean keepLinkHandle) throws IOException { - String response = makeServiceRequest(USERNAME_LINK_PATH, "PUT", JsonUtil.toJson(new SetUsernameLinkRequestBody(encryptedUsername, keepLinkHandle))); - SetUsernameLinkResponseBody parsed = JsonUtil.fromJson(response, SetUsernameLinkResponseBody.class); - - return parsed.getUsernameLinkHandle(); - } - - /** Deletes your active username link. */ - public void deleteUsernameLink() throws IOException { - makeServiceRequest(USERNAME_LINK_PATH, "DELETE", null); - } - - /** Given a link serverId (see {@link #createUsernameLink(String, boolean)}}), this will return the encrypted username associate with the link. */ - public byte[] getEncryptedUsernameFromLinkServerId(UUID serverId) throws IOException { - String response = makeServiceRequestWithoutAuthentication(String.format(USERNAME_FROM_LINK_PATH, serverId.toString()), "GET", null); - GetUsernameFromLinkResponseBody parsed = JsonUtil.fromJson(response, GetUsernameFromLinkResponseBody.class); - - return Base64.decode(parsed.getUsernameLinkEncryptedValue()); - } - - public void deleteAccount() throws IOException { - makeServiceRequest(DELETE_ACCOUNT_PATH, "DELETE", null); - } - - public void setDeviceName(int deviceId, @Nonnull SetDeviceNameRequest request) throws IOException { - String body = JsonUtil.toJson(request); - makeServiceRequest(String.format(Locale.US, SET_DEVICE_NAME_PATH, deviceId), "PUT", body); - } - public void requestRateLimitPushChallenge() throws IOException { makeServiceRequest(REQUEST_RATE_LIMIT_PUSH_CHALLENGE, "POST", ""); } @@ -2713,18 +2482,7 @@ public class PushServiceSocket { public enum VerificationCodeTransport { SMS, VOICE } - private static class RegistrationLock { - @JsonProperty - private String pin; - - public RegistrationLock() {} - - public RegistrationLock(String pin) { - this.pin = pin; - } - } - - private static class RegistrationLockV2 { + public static class RegistrationLockV2 { @JsonProperty private String registrationLock; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameRequest.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameRequest.java index eec41934ab..6023c249e6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameRequest.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReserveUsernameRequest.java @@ -5,11 +5,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collections; import java.util.List; -class ReserveUsernameRequest { +public class ReserveUsernameRequest { @JsonProperty private List usernameHashes; - ReserveUsernameRequest(List usernameHashes) { + public ReserveUsernameRequest(List usernameHashes) { this.usernameHashes = Collections.unmodifiableList(usernameHashes); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java index 052d5eb3fc..dfbcd2e0c9 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/util/JsonUtil.java @@ -19,15 +19,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.module.kotlin.KotlinModule; +import org.signal.core.util.Base64; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.logging.Log; import org.whispersystems.signalservice.api.kbs.MasterKey; -import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceId; +import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; import org.whispersystems.signalservice.api.util.UuidUtil; -import org.signal.core.util.Base64; import java.io.IOException; import java.util.UUID; @@ -73,6 +73,12 @@ public class JsonUtil { return objectMapper.readValue(json, typeRef); } + public static T fromJson(byte[] json, Class clazz) + throws IOException + { + return objectMapper.readValue(json, clazz); + } + public static T fromJsonResponse(String json, TypeReference typeRef) throws MalformedResponseException { @@ -92,6 +98,16 @@ public class JsonUtil { throw new MalformedResponseException("Unable to parse entity", e); } } + + public static T fromJsonResponse(byte[] body, Class clazz) + throws MalformedResponseException + { + try { + return JsonUtil.fromJson(body, clazz); + } catch (IOException e) { + throw new MalformedResponseException("Unable to parse entity", e); + } + } public static class IdentityKeySerializer extends JsonSerializer { @Override