diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.kt index dc0175cbc8..4b65faeabd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.kt @@ -4,6 +4,7 @@ import android.text.TextUtils import org.signal.core.util.Base64 import org.signal.core.util.Util import org.signal.core.util.logging.Log +import org.signal.libsignal.net.RequestResult import org.signal.libsignal.usernames.BaseUsernameException import org.signal.libsignal.usernames.Username import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential @@ -26,7 +27,6 @@ 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.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 @@ -384,9 +384,7 @@ class RefreshOwnProfileJob private constructor(parameters: Parameters) : BaseJob val displayBadgesOnProfile = SignalStore.inAppPayments.getDisplayBadgesOnProfile() Log.d( TAG, - "Detected mixed visibility of badges. Telling the server to mark them all " + - (if (displayBadgesOnProfile) "" else "not") + - " visible.", + "Detected mixed visibility of badges. Telling the server to mark them all ${if (displayBadgesOnProfile) "" else "not"} visible.", true ) @@ -399,8 +397,6 @@ class RefreshOwnProfileJob private constructor(parameters: Parameters) : BaseJob } private fun checkUsernameIsInSync() { - var validated = false - try { val localUsername = SignalStore.account.username @@ -429,32 +425,37 @@ class RefreshOwnProfileJob private constructor(parameters: Parameters) : BaseJob return } - try { - val localUsernameLink = SignalStore.account.usernameLink + val localUsernameLink = SignalStore.account.usernameLink ?: return - if (localUsernameLink != null) { - val remoteEncryptedUsername = NetworkResultUtil.toBasicLegacy(SignalNetwork.username.getEncryptedUsernameFromLinkServerId(localUsernameLink.serverId)) - val combinedLink = Username.UsernameLink(localUsernameLink.entropy, remoteEncryptedUsername) - val remoteUsername = Username.fromLink(combinedLink) + when (val usernameFetchResult = SignalNetwork.username.getDecryptedUsernameFromLinkServerIdAndEntropy(localUsernameLink.serverId, localUsernameLink.entropy)) { + is RequestResult.Success -> { + val remoteUsername = usernameFetchResult.result + + if (remoteUsername == null) { + Log.w(TAG, "Local username link was not found on remote. Marking as mismatched.") + UsernameRepository.onUsernameLinkMismatchDetected() + return + } if (remoteUsername.getUsername() != SignalStore.account.username) { Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!") UsernameRepository.onUsernameLinkMismatchDetected() - } else { - Log.d(TAG, "Username link validated.") + return } - validated = true + Log.d(TAG, "Username link validated.") + UsernameRepository.onUsernameConsistencyValidated() + } + is RequestResult.NonSuccess -> { + Log.w(TAG, "Failed to decrypt username link using our local link data. ${usernameFetchResult.error}") + UsernameRepository.onUsernameLinkMismatchDetected() + } + is RequestResult.RetryableNetworkError -> { + Log.w(TAG, "Failed perform synchronization check during the username link phase, skipping.", usernameFetchResult.networkError) + } + is RequestResult.ApplicationError -> { + throw usernameFetchResult.cause } - } catch (e: IOException) { - Log.w(TAG, "Failed perform synchronization check during the username link phase.", e) - } catch (e: BaseUsernameException) { - Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e) - UsernameRepository.onUsernameLinkMismatchDetected() - } - - if (validated) { - UsernameRepository.onUsernameConsistencyValidated() } } 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 49bafa2a30..89dedea97a 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 @@ -14,6 +14,8 @@ import org.signal.core.util.toByteArray import org.signal.libsignal.net.RequestResult import org.signal.libsignal.usernames.BaseUsernameException import org.signal.libsignal.usernames.Username +import org.signal.libsignal.usernames.UsernameLinkInvalidEntropyDataLength +import org.signal.libsignal.usernames.UsernameLinkInvalidLinkData import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkResetResult import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -234,25 +236,24 @@ object UsernameRepository { return Single .fromCallable { - 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 + val username = when (val result = SignalNetwork.username.getDecryptedUsernameFromLinkServerIdAndEntropy(components.serverId, components.entropy)) { + is RequestResult.Success -> + result.result ?: return@fromCallable UsernameLinkConversionResult.NotFound(null) + is RequestResult.NonSuccess -> { + when (result.error) { + is UsernameLinkInvalidEntropyDataLength, + is UsernameLinkInvalidLinkData -> { + Log.w(TAG, "[convertLinkToUsername] Bad username conversion. ${result.error}") + return@fromCallable UsernameLinkConversionResult.Invalid + } } } - is NetworkResult.NetworkError -> return@fromCallable UsernameLinkConversionResult.NetworkError - is NetworkResult.ApplicationError -> throw result.throwable - } - - 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 + is RequestResult.RetryableNetworkError -> { + return@fromCallable UsernameLinkConversionResult.NetworkError + } + is RequestResult.ApplicationError -> { + throw result.cause + } } when (val result = SignalNetwork.username.getAciByUsername(username)) { diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt index c3efb355af..0fd8a215d9 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/username/UsernameApi.kt @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api.username import kotlinx.coroutines.runBlocking import org.signal.core.models.ServiceId import org.signal.core.util.Base64 +import org.signal.libsignal.net.LookUpUsernameLinkFailure import org.signal.libsignal.net.RequestResult import org.signal.libsignal.net.UnauthUsernamesService import org.signal.libsignal.net.getOrError @@ -27,7 +28,10 @@ import java.util.UUID class UsernameApi(private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket) { /** - * Gets the ACI for the given [username], if it exists. This is an unauthenticated request. + * Gets the ACI for the given [username]. This is an unauthenticated request. + * + * A successful result with a null value means the username was not found on the server. + * Other errors (network, decryption, etc.) are represented by the other [RequestResult] types. */ fun getAciByUsername(username: Username): RequestResult { return runBlocking { @@ -38,18 +42,16 @@ class UsernameApi(private val unauthWebSocket: SignalWebSocket.UnauthenticatedWe } /** - * Given a link serverId, this will return the encrypted username associated with the link. + * Gets the username for a ([serverId], [entropy]) pairing from a username link. This is an unauthenticated request. * - * 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 + * A successful result with a null value means no username link was found for the given server ID. + * Other errors (network, decryption, etc.) are represented by the other [RequestResult] types. */ - 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) } + fun getDecryptedUsernameFromLinkServerIdAndEntropy(serverId: UUID, entropy: ByteArray): RequestResult { + return runBlocking { + unauthWebSocket.runCatchingWithUnauthChatConnection { chatConnection -> + UnauthUsernamesService(chatConnection).lookUpUsernameLink(serverId, entropy) + }.getOrError() + } } }