Centralize username logic in UsernameRepository.

This commit is contained in:
Greyson Parrelli
2023-11-08 14:56:33 -05:00
committed by Cody Henthorne
parent 0f4f87067e
commit e5ab5241d5
9 changed files with 105 additions and 158 deletions

View File

@@ -22,6 +22,7 @@ import androidx.fragment.app.FragmentActivity;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.RxExtensions;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
@@ -42,6 +43,9 @@ import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoin
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.joining.GroupJoinUpdateRequiredBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameLinkConversionResult;
import org.thoughtcrime.securesms.proxy.ProxyBottomSheetFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
@@ -296,15 +300,12 @@ public class CommunicationActions {
*/
public static void handlePotentialSignalMeUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) {
String e164 = SignalMeUtil.parseE164FromLink(activity, potentialUrl);
UsernameLinkComponents username = SignalMeUtil.parseUsernameComponentsFromLink(potentialUrl);
UsernameLinkComponents username = UsernameRepository.parseLink(potentialUrl);
if (e164 != null) {
handleE164Link(activity, e164);
} else if (username != null) {
handleUsernameLink(activity, username);
}
if (e164 != null || username != null) {
handleUsernameLink(activity, potentialUrl);
}
}
@@ -460,25 +461,21 @@ public class CommunicationActions {
});
}
private static void handleUsernameLink(Activity activity, UsernameLinkComponents link) {
private static void handleUsernameLink(Activity activity, String link) {
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500);
SimpleTask.run(() -> {
try {
byte[] encryptedUsername = ApplicationDependencies.getSignalServiceAccountManager().getEncryptedUsernameFromLinkServerId(link.getServerId());
Username username = Username.fromLink(new Username.UsernameLink(link.getEntropy(), encryptedUsername));
Optional<ServiceId> serviceId = UsernameUtil.fetchAciForUsername(username.getUsername());
UsernameLinkConversionResult result = RxExtensions.safeBlockingGet(UsernameRepository.fetchUsernameAndAciFromLink(link));
if (serviceId.isPresent()) {
return Recipient.externalUsername(serviceId.get(), username.getUsername());
// TODO we could be better here and report different types of errors to the UI
if (result instanceof UsernameLinkConversionResult.Success success) {
return Recipient.externalUsername(success.getAci(), success.getUsername().getUsername());
} else {
return null;
}
} catch (IOException e) {
Log.w(TAG, "Failed to fetch encrypted username", e);
return null;
} catch (BaseUsernameException e) {
Log.w(TAG, "Invalid username", e);
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted?", e);
return null;
}
}, recipient -> {

View File

@@ -2,17 +2,11 @@ package org.thoughtcrime.securesms.util
import android.content.Context
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.util.UuidUtil
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.util.Locale
internal object SignalMeUtil {
private val E164_REGEX = """^(https|sgnl)://signal\.me/#p/(\+[0-9]+)$""".toRegex()
private val USERNAME_REGEX = """^(https|sgnl)://signal\.me/#eu/(.+)$""".toRegex()
/**
* If this is a valid signal.me link and has a valid e164, it will return the e164. Otherwise, it will return null.
@@ -33,31 +27,4 @@ internal object SignalMeUtil {
}
}
}
/**
* If this is a valid signal.me link and has valid username link components, it will return those components. Otherwise, it will return null.
*/
@JvmStatic
fun parseUsernameComponentsFromLink(link: String?): UsernameLinkComponents? {
if (link.isNullOrBlank()) {
return null
}
return USERNAME_REGEX.find(link)?.let { match ->
val usernameLinkBase64: String = match.groups[2]?.value ?: return@let null
try {
val usernameLinkData: ByteArray = Base64.decode(usernameLinkBase64).takeIf { it.size == 48 } ?: return@let null
val entropy: ByteArray = usernameLinkData.sliceArray(0 until 32)
val uuidBytes: ByteArray = usernameLinkData.sliceArray(32 until usernameLinkData.size)
val uuid = UuidUtil.parseOrNull(uuidBytes)
UsernameLinkComponents(entropy, uuid)
} catch (e: UnsupportedEncodingException) {
null
} catch (e: IOException) {
null
}
}
}
}

View File

@@ -1,21 +1,7 @@
package org.thoughtcrime.securesms.util
import androidx.annotation.WorkerThread
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.libsignal.usernames.BaseUsernameException
import org.signal.libsignal.usernames.Username
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.IOException
import java.util.Locale
import java.util.Optional
import java.util.UUID
import java.util.regex.Pattern
object UsernameUtil {
@@ -24,24 +10,29 @@ object UsernameUtil {
const val MAX_LENGTH = 32
private val FULL_PATTERN = Pattern.compile(String.format(Locale.US, "^[a-zA-Z_][a-zA-Z0-9_]{%d,%d}$", MIN_LENGTH - 1, MAX_LENGTH - 1), Pattern.CASE_INSENSITIVE)
private val DIGIT_START_PATTERN = Pattern.compile("^[0-9].*$")
private val URL_PATTERN = """(https://)?signal.me/?#eu/([a-zA-Z0-9+\-_/]+)""".toRegex()
private const val BASE_URL_SCHEMELESS = "signal.me/#eu/"
private const val BASE_URL = "https://$BASE_URL_SCHEMELESS"
private val SEARCH_PATTERN = Pattern.compile(
String.format(
Locale.US,
"^[a-zA-Z_][a-zA-Z0-9_]{%d,%d}(.[0-9]+)?$",
"^@?[a-zA-Z_][a-zA-Z0-9_]{%d,%d}(.[0-9]+)?$",
MIN_LENGTH - 1,
MAX_LENGTH - 1,
Pattern.CASE_INSENSITIVE
)
)
@JvmStatic
fun isValidUsernameForSearch(value: String): Boolean {
return value.isNotEmpty() && SEARCH_PATTERN.matcher(value).matches()
}
@JvmStatic
fun sanitizeUsernameFromSearch(value: String): String {
return value.replace("[^a-zA-Z0-9_.]".toRegex(), "")
}
@JvmStatic
fun checkUsername(value: String?): InvalidReason? {
return when {
@@ -66,81 +57,6 @@ object UsernameUtil {
}
}
@JvmStatic
@WorkerThread
fun fetchAciForUsername(username: String): Optional<ServiceId> {
val localId = recipients.getByUsername(username)
if (localId.isPresent) {
val recipient = Recipient.resolved(localId.get())
if (recipient.serviceId.isPresent) {
Log.i(TAG, "Found username locally -- using associated UUID.")
return recipient.serviceId
} else {
Log.w(TAG, "Found username locally, but it had no associated UUID! Clearing it.")
recipients.clearUsernameIfExists(username)
}
}
Log.d(TAG, "No local user with this username. Searching remotely.")
return try {
fetchAciForUsernameHash(Base64.encodeUrlSafeWithoutPadding(Username(username).hash))
} catch (e: BaseUsernameException) {
Optional.empty()
}
}
/**
* Hashes a username to a url-safe base64 string.
* @throws BaseUsernameException If the username is invalid and un-hashable.
*/
@Throws(BaseUsernameException::class)
fun hashUsernameToBase64(username: String?): String {
return Base64.encodeUrlSafeWithoutPadding(Username.hash(username))
}
@JvmStatic
@WorkerThread
fun fetchAciForUsernameHash(base64UrlSafeEncodedUsernameHash: String): Optional<ServiceId> {
return try {
val aci = ApplicationDependencies.getSignalServiceAccountManager().getAciByUsernameHash(base64UrlSafeEncodedUsernameHash)
Optional.ofNullable(aci)
} catch (e: IOException) {
Log.w(TAG, "Failed to get ACI for username hash", e)
Optional.empty()
}
}
/**
* Generates a username link from the provided [UsernameLinkComponents].
*/
fun generateLink(components: UsernameLinkComponents): String {
val combined: ByteArray = components.entropy + components.serverId.toByteArray()
val base64 = Base64.encodeUrlSafeWithoutPadding(combined)
return BASE_URL + base64
}
/**
* Parses out the [UsernameLinkComponents] from a link if possible, otherwise null.
* You need to make a separate network request to convert these components into a username.
*/
fun parseLink(url: String): UsernameLinkComponents? {
val match: MatchResult = URL_PATTERN.find(url) ?: return null
val path: String = match.groups[2]?.value ?: return null
val allBytes: ByteArray = Base64.decode(path)
if (allBytes.size != 48) {
return null
}
val entropy: ByteArray = allBytes.slice(0 until 32).toByteArray()
val serverId: ByteArray = allBytes.slice(32 until allBytes.size).toByteArray()
val serverIdUuid: UUID = UuidUtil.parseOrNull(serverId) ?: return null
return UsernameLinkComponents(entropy = entropy, serverId = serverIdUuid)
}
enum class InvalidReason {
TOO_SHORT,
TOO_LONG,