Centralize username logic in UsernameRepository.
This commit is contained in:
parent
0f4f87067e
commit
e5ab5241d5
9 changed files with 105 additions and 158 deletions
|
@ -50,6 +50,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.RxExtensions;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
|
@ -70,6 +71,8 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
|||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
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.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
|
@ -682,11 +685,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchAciForUsername(username);
|
||||
}, uuid -> {
|
||||
try {
|
||||
return RxExtensions.safeBlockingGet(UsernameRepository.fetchAciForUsername(UsernameUtil.sanitizeUsernameFromSearch(username)));
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted?", e);
|
||||
return UsernameAciFetchResult.NetworkError.INSTANCE;
|
||||
}
|
||||
}, result -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), username);
|
||||
|
||||
// TODO Could be more specific with errors
|
||||
if (result instanceof UsernameAciFetchResult.Success success) {
|
||||
Recipient recipient = Recipient.externalUsername(success.getAci(), username);
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), username);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
|
|
|
@ -15,9 +15,9 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeSt
|
|||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.toLink
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
|
||||
class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
||||
|
||||
|
@ -39,7 +39,7 @@ class UsernameLinkQrColorPickerViewModel : ViewModel() {
|
|||
|
||||
if (usernameLink != null) {
|
||||
disposable += Single
|
||||
.fromCallable { QrCodeData.forData(UsernameUtil.generateLink(usernameLink), 64) }
|
||||
.fromCallable { QrCodeData.forData(usernameLink.toLink(), 64) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
|
|
|
@ -34,9 +34,9 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.Use
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.toLink
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
|
||||
import java.util.Optional
|
||||
|
||||
|
@ -48,7 +48,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
|||
UsernameLinkSettingsState(
|
||||
activeTab = ActiveTab.Code,
|
||||
username = SignalStore.account().username!!,
|
||||
usernameLinkState = SignalStore.account().usernameLink?.let { UsernameLinkState.Present(UsernameUtil.generateLink(it)) } ?: UsernameLinkState.NotSet,
|
||||
usernameLinkState = SignalStore.account().usernameLink?.let { UsernameLinkState.Present(it.toLink()) } ?: UsernameLinkState.NotSet,
|
||||
qrCodeState = QrCodeState.Loading,
|
||||
qrCodeColorScheme = SignalStore.misc().usernameQrCodeColorScheme
|
||||
)
|
||||
|
@ -61,7 +61,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
|||
init {
|
||||
disposable += usernameLink
|
||||
.observeOn(Schedulers.io())
|
||||
.map { link -> link.map { UsernameUtil.generateLink(it) } }
|
||||
.map { link -> link.map { it.toLink() } }
|
||||
.flatMapSingle { generateQrCodeData(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { qrData ->
|
||||
|
@ -122,7 +122,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
|||
|
||||
_state.value = _state.value.copy(
|
||||
usernameLinkState = if (components.isPresent) {
|
||||
val link = UsernameUtil.generateLink(components.get())
|
||||
val link = components.get().toLink()
|
||||
UsernameLinkState.Present(link)
|
||||
} else {
|
||||
UsernameLinkState.NotSet
|
||||
|
@ -152,7 +152,7 @@ class UsernameLinkSettingsViewModel : ViewModel() {
|
|||
indeterminateProgress = true
|
||||
)
|
||||
|
||||
disposable += UsernameRepository.convertLinkToUsernameAndAci(url)
|
||||
disposable += UsernameRepository.fetchUsernameAndAciFromLink(url)
|
||||
.map { result ->
|
||||
when (result) {
|
||||
is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(Recipient.externalUsername(result.aci, result.username.toString()))
|
||||
|
|
|
@ -23,10 +23,14 @@ 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.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.UuidUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Performs various actions around usernames and username links.
|
||||
|
@ -77,6 +81,10 @@ import java.io.IOException
|
|||
object UsernameRepository {
|
||||
private val TAG = Log.tag(UsernameRepository::class.java)
|
||||
|
||||
private val URL_REGEX = """(https://)?signal.me/?#eu/([a-zA-Z0-9+\-_/]+)""".toRegex()
|
||||
|
||||
val BASE_URL = "https://signal.me/#eu/"
|
||||
|
||||
private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager()
|
||||
|
||||
/**
|
||||
|
@ -163,8 +171,9 @@ object UsernameRepository {
|
|||
/**
|
||||
* Given a full username link, this will do the necessary parsing and network lookups to resolve it to a (username, ACI) pair.
|
||||
*/
|
||||
fun convertLinkToUsernameAndAci(url: String): Single<UsernameLinkConversionResult> {
|
||||
val components: UsernameLinkComponents = UsernameUtil.parseLink(url) ?: return Single.just(UsernameLinkConversionResult.Invalid)
|
||||
@JvmStatic
|
||||
fun fetchUsernameAndAciFromLink(url: String): Single<UsernameLinkConversionResult> {
|
||||
val components: UsernameLinkComponents = parseLink(url) ?: return Single.just(UsernameLinkConversionResult.Invalid)
|
||||
|
||||
return Single
|
||||
.fromCallable {
|
||||
|
@ -176,7 +185,7 @@ object UsernameRepository {
|
|||
|
||||
username = Username.fromLink(link)
|
||||
|
||||
val aci = accountManager.getAciByUsernameHash(UsernameUtil.hashUsernameToBase64(username.toString()))
|
||||
val aci = accountManager.getAciByUsername(username)
|
||||
|
||||
UsernameLinkConversionResult.Success(username, aci)
|
||||
} catch (e: IOException) {
|
||||
|
@ -199,6 +208,49 @@ object UsernameRepository {
|
|||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fetchAciForUsername(username: String): Single<UsernameAciFetchResult> {
|
||||
return Single.fromCallable {
|
||||
try {
|
||||
val aci: ACI = ApplicationDependencies.getSignalServiceAccountManager().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: IOException) {
|
||||
Log.w(TAG, "[fetchAciFromUsername] Hit network error while trying to resolve ACI from username", e)
|
||||
UsernameAciFetchResult.NetworkError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parseLink(url: String): UsernameLinkComponents? {
|
||||
val match: MatchResult = URL_REGEX.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)
|
||||
}
|
||||
|
||||
fun UsernameLinkComponents.toLink(): String {
|
||||
val combined: ByteArray = this.entropy + this.serverId.toByteArray()
|
||||
val base64 = Base64.encodeUrlSafeWithoutPadding(combined)
|
||||
return BASE_URL + base64
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun reserveUsernameInternal(nickname: String): Result<UsernameState.Reserved, UsernameSetResult> {
|
||||
return try {
|
||||
|
@ -320,4 +372,10 @@ object UsernameRepository {
|
|||
/** No user exists for the given link. */
|
||||
data class NotFound(val username: Username?) : UsernameLinkConversionResult()
|
||||
}
|
||||
|
||||
sealed class UsernameAciFetchResult {
|
||||
class Success(val aci: ACI) : UsernameAciFetchResult()
|
||||
object NotFound : UsernameAciFetchResult()
|
||||
object NetworkError : UsernameAciFetchResult()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -84,7 +84,6 @@ import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper
|
|||
import org.signal.core.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -761,8 +760,8 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public ACI getAciByUsernameHash(String usernameHash) throws IOException {
|
||||
return this.pushServiceSocket.getAciByUsernameHash(usernameHash);
|
||||
public ACI getAciByUsername(Username username) throws IOException {
|
||||
return this.pushServiceSocket.getAciByUsernameHash(Base64.encodeUrlSafeWithoutPadding(username.getHash()));
|
||||
}
|
||||
|
||||
public ReserveUsernameResponse reserveUsername(List<String> usernameHashes) throws IOException {
|
||||
|
|
|
@ -1068,7 +1068,7 @@ public class PushServiceSocket {
|
|||
byte[] randomness = new byte[32];
|
||||
random.nextBytes(randomness);
|
||||
|
||||
byte[] proof = Username.generateProof(username, randomness);
|
||||
byte[] proof = new Username(username).generateProofWithRandomness(randomness);
|
||||
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(reserveUsernameResponse.getUsernameHash(),
|
||||
Base64.encodeUrlSafeWithoutPadding(proof));
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue