mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Centralize username logic in UsernameRepository.
This commit is contained in:
committed by
Cody Henthorne
parent
0f4f87067e
commit
e5ab5241d5
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user