username links API

This commit is contained in:
Sergey Skrobotov
2023-06-02 10:15:09 -07:00
parent ecd207f0a1
commit 47cc7fd615
13 changed files with 653 additions and 142 deletions

View File

@@ -20,6 +20,8 @@ import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.IOException;
import java.security.SecureRandom;
import java.time.Clock;
@@ -32,6 +34,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletionException;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
@@ -77,6 +80,7 @@ import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.DeviceName;
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
@@ -85,6 +89,7 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.UsernameLinkHandle;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@@ -421,7 +426,7 @@ public class AccountController {
}
if (availableForTransfer.orElse(false) && existingAccount.map(Account::isTransferSupported).orElse(false)) {
throw new WebApplicationException(Response.status(409).build());
throw new WebApplicationException(Status.CONFLICT);
}
rateLimiters.getVerifyLimiter().clear(number);
@@ -699,7 +704,8 @@ public class AccountController {
@DELETE
@Path("/username_hash")
@Produces(MediaType.APPLICATION_JSON)
public void deleteUsernameHash(@Auth AuthenticatedAccount auth) {
public void deleteUsernameHash(final @Auth AuthenticatedAccount auth) {
clearUsernameLink(auth.getAccount());
accounts.clearUsernameHash(auth.getAccount());
}
@@ -736,9 +742,10 @@ public class AccountController {
@Path("/username_hash/confirm")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public UsernameHashResponse confirmUsernameHash(@Auth AuthenticatedAccount auth,
@HeaderParam(HeaderUtils.X_SIGNAL_AGENT) String userAgent,
@NotNull @Valid ConfirmUsernameHashRequest confirmRequest) throws RateLimitExceededException {
public UsernameHashResponse confirmUsernameHash(
@Auth final AuthenticatedAccount auth,
@HeaderParam(HeaderUtils.X_SIGNAL_AGENT) final String userAgent,
@NotNull @Valid final ConfirmUsernameHashRequest confirmRequest) throws RateLimitExceededException {
rateLimiters.getUsernameSetLimiter().validate(auth.getAccount().getUuid());
try {
@@ -747,6 +754,11 @@ public class AccountController {
throw new WebApplicationException(Response.status(422).build());
}
// Whenever a valid request for a username change arrives,
// we're making sure to clear username link. This may happen before and username changes are written to the db
// but verifying zk proof means that request itself is valid from the client's perspective
clearUsernameLink(auth.getAccount());
try {
final Account account = accounts.confirmReservedUsernameHash(auth.getAccount(), confirmRequest.usernameHash());
return account
@@ -796,6 +808,90 @@ public class AccountController {
.orElseThrow(() -> new WebApplicationException(Status.NOT_FOUND));
}
@Timed
@PUT
@Path("/username_link")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Operation(
summary = "Set username link",
description = """
Authenticated endpoint. For the given encrypted username generates a username link handle.
Username link handle could be used to lookup the encrypted username.
An account can only have one username link at a time. Calling this endpoint will reset previously stored
encrypted username and deactivate previous link handle.
"""
)
@ApiResponse(responseCode = "200", description = "Username Link updated successfully.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
@ApiResponse(responseCode = "409", description = "Username is not set for the account.")
@ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Ratelimited.")
public UsernameLinkHandle updateUsernameLink(
@Auth final AuthenticatedAccount auth,
@NotNull @Valid final EncryptedUsername encryptedUsername) throws RateLimitExceededException {
// check ratelimiter for username link operations
rateLimiters.forDescriptor(RateLimiters.For.USERNAME_LINK_OPERATION).validate(auth.getAccount().getUuid());
// check if username hash is set for the account
if (auth.getAccount().getUsernameHash().isEmpty()) {
throw new WebApplicationException(Status.CONFLICT);
}
final UUID usernameLinkHandle = UUID.randomUUID();
updateUsernameLink(auth.getAccount(), usernameLinkHandle, encryptedUsername.usernameLinkEncryptedValue());
return new UsernameLinkHandle(usernameLinkHandle);
}
@Timed
@DELETE
@Path("/username_link")
@Operation(
summary = "Delete username link",
description = """
Authenticated endpoint. Deletes username link for the given account: previously store encrypted username is deleted
and username link handle is deactivated.
"""
)
@ApiResponse(responseCode = "204", description = "Username Link successfully deleted.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
@ApiResponse(responseCode = "429", description = "Ratelimited.")
public void deleteUsernameLink(@Auth final AuthenticatedAccount auth) throws RateLimitExceededException {
// check ratelimiter for username link operations
rateLimiters.forDescriptor(RateLimiters.For.USERNAME_LINK_OPERATION).validate(auth.getAccount().getUuid());
clearUsernameLink(auth.getAccount());
}
@Timed
@GET
@Path("/username_link/{uuid}")
@Produces(MediaType.APPLICATION_JSON)
@RateLimitedByIp(RateLimiters.For.USERNAME_LINK_LOOKUP_PER_IP)
@Operation(
summary = "Lookup username link",
description = """
Enforced unauthenticated endpoint. For the given username link handle, looks up the database for an associated encrypted username.
If found, encrypted username is returned, otherwise responds with 404 Not Found.
"""
)
@ApiResponse(responseCode = "200", description = "Username link with the given handle was found.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "404", description = "Username link was not found for the given handle.")
@ApiResponse(responseCode = "422", description = "Invalid request format.")
@ApiResponse(responseCode = "429", description = "Ratelimited.")
public EncryptedUsername lookupUsernameLink(
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
@PathParam("uuid") final UUID usernameLinkHandle) {
final Optional<byte[]> maybeEncryptedUsername = accounts.getByUsernameLinkHandle(usernameLinkHandle)
.flatMap(Account::getEncryptedUsername);
if (authenticatedAccount.isPresent()) {
throw new ForbiddenException("must not use authenticated connection for connection graph revealing operations");
}
if (maybeEncryptedUsername.isEmpty()) {
throw new WebApplicationException(Status.NOT_FOUND);
}
return new EncryptedUsername(maybeEncryptedUsername.get());
}
@HEAD
@Path("/account/{uuid}")
@RateLimitedByIp(RateLimiters.For.CHECK_ACCOUNT_EXISTENCE)
@@ -915,6 +1011,20 @@ public class AccountController {
}
}
private void clearUsernameLink(final Account account) {
updateUsernameLink(account, null, null);
}
private void updateUsernameLink(
final Account account,
@Nullable final UUID usernameLinkHandle,
@Nullable final byte[] encryptedUsername) {
if ((encryptedUsername == null) ^ (usernameLinkHandle == null)) {
throw new IllegalStateException("Both or neither arguments must be null");
}
accounts.update(account, a -> a.setUsernameLinkDetails(usernameLinkHandle, encryptedUsername));
}
private void rethrowRateLimitException(final CompletionException completionException)
throws RateLimitExceededException {

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public record EncryptedUsername(@NotNull @Size(min = 1, max = 128) byte[] usernameLinkEncryptedValue) {
}

View File

@@ -0,0 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import java.util.UUID;
import javax.validation.constraints.NotNull;
public record UsernameLinkHandle(@NotNull UUID usernameLinkHandle) {
}

View File

@@ -37,6 +37,8 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
USERNAME_LOOKUP("usernameLookup", false, new RateLimiterConfig(100, Duration.ofSeconds(15))),
USERNAME_SET("usernameSet", false, new RateLimiterConfig(100, Duration.ofSeconds(15))),
USERNAME_RESERVE("usernameReserve", false, new RateLimiterConfig(100, Duration.ofSeconds(15))),
USERNAME_LINK_OPERATION("usernameLinkOperation", false, new RateLimiterConfig(10, Duration.ofMinutes(1))),
USERNAME_LINK_LOOKUP_PER_IP("usernameLinkLookupPerIp", false, new RateLimiterConfig(100, Duration.ofSeconds(15))),
CHECK_ACCOUNT_EXISTENCE("checkAccountExistence", false, new RateLimiterConfig(1000, Duration.ofMillis(60))),
REGISTRATION("registration", false, new RateLimiterConfig(6, Duration.ofMillis(500))),
VERIFICATION_PUSH_CHALLENGE("verificationPushChallenge", false, new RateLimiterConfig(5, Duration.ofMillis(500))),

View File

@@ -7,6 +7,8 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
@@ -16,8 +18,6 @@ import java.util.Optional;
import java.util.UUID;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
@@ -54,6 +54,14 @@ public class Account {
@Nullable
private byte[] reservedUsernameHash;
@JsonIgnore
@Nullable
private UUID usernameLinkHandle;
@JsonProperty("eu")
@Nullable
private byte[] encryptedUsername;
@JsonProperty
private List<Device> devices = new ArrayList<>();
@@ -162,6 +170,38 @@ public class Account {
this.reservedUsernameHash = reservedUsernameHash;
}
@Nullable
public UUID getUsernameLinkHandle() {
requireNotStale();
return usernameLinkHandle;
}
public Optional<byte[]> getEncryptedUsername() {
requireNotStale();
return Optional.ofNullable(encryptedUsername);
}
public void setUsernameLinkDetails(@Nullable final UUID usernameLinkHandle, @Nullable final byte[] encryptedUsername) {
requireNotStale();
if ((usernameLinkHandle == null) ^ (encryptedUsername == null)) {
throw new IllegalArgumentException("Both or neither arguments must be null");
}
if (usernameHash == null && encryptedUsername != null) {
throw new IllegalArgumentException("usernameHash field must be set to store username link");
}
this.encryptedUsername = encryptedUsername;
this.usernameLinkHandle = usernameLinkHandle;
}
/*
* This method is intentionally left package-private so that it's only used
* when Account is read from DB
*/
void setUsernameLinkHandle(@Nullable final UUID usernameLinkHandle) {
requireNotStale();
this.usernameLinkHandle = usernameLinkHandle;
}
public void addDevice(Device device) {
requireNotStale();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
@@ -45,6 +45,8 @@ import software.amazon.awssdk.services.dynamodb.model.Delete;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.QueryResponse;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
@@ -68,6 +70,7 @@ public class Accounts extends AbstractDynamoDbStore {
private static final Timer UPDATE_TIMER = Metrics.timer(name(Accounts.class, "update"));
private static final Timer GET_BY_NUMBER_TIMER = Metrics.timer(name(Accounts.class, "getByNumber"));
private static final Timer GET_BY_USERNAME_HASH_TIMER = Metrics.timer(name(Accounts.class, "getByUsernameHash"));
private static final Timer GET_BY_USERNAME_LINK_HANDLE_TIMER = Metrics.timer(name(Accounts.class, "getByUsernameLinkHandle"));
private static final Timer GET_BY_PNI_TIMER = Metrics.timer(name(Accounts.class, "getByPni"));
private static final Timer GET_BY_UUID_TIMER = Metrics.timer(name(Accounts.class, "getByUuid"));
private static final Timer GET_ALL_FROM_START_TIMER = Metrics.timer(name(Accounts.class, "getAllFrom"));
@@ -82,6 +85,8 @@ public class Accounts extends AbstractDynamoDbStore {
static final String KEY_ACCOUNT_UUID = "U";
// uuid, attribute on account table, primary key for PNI table
static final String ATTR_PNI_UUID = "PNI";
// uuid of the current username link or null
static final String ATTR_USERNAME_LINK_UUID = "UL";
// phone number
static final String ATTR_ACCOUNT_E164 = "P";
// account, serialized to JSON
@@ -99,6 +104,8 @@ public class Accounts extends AbstractDynamoDbStore {
// time to live; number
static final String ATTR_TTL = "TTL";
static final String USERNAME_LINK_TO_UUID_INDEX = "ul_to_u";
private final Clock clock;
private final DynamoDbAsyncClient asyncClient;
@@ -525,20 +532,28 @@ public class Accounts extends AbstractDynamoDbStore {
":version", AttributeValues.fromInt(account.getVersion()),
":version_increment", AttributeValues.fromInt(1)));
final String updateExpression;
final StringBuilder updateExpressionBuilder = new StringBuilder("SET #data = :data, #cds = :cds");
if (account.getUnidentifiedAccessKey().isPresent()) {
// if it's present in the account, also set the uak
attrNames.put("#uak", ATTR_UAK);
attrValues.put(":uak", AttributeValues.fromByteArray(account.getUnidentifiedAccessKey().get()));
updateExpression = "SET #data = :data, #cds = :cds, #uak = :uak ADD #version :version_increment";
} else {
updateExpression = "SET #data = :data, #cds = :cds ADD #version :version_increment";
updateExpressionBuilder.append(", #uak = :uak");
}
if (account.getEncryptedUsername().isPresent() && account.getUsernameLinkHandle() != null) {
attrNames.put("#ul", ATTR_USERNAME_LINK_UUID);
attrValues.put(":ul", AttributeValues.fromUUID(account.getUsernameLinkHandle()));
updateExpressionBuilder.append(", #ul = :ul");
}
updateExpressionBuilder.append(" ADD #version :version_increment");
if (account.getEncryptedUsername().isEmpty() || account.getUsernameLinkHandle() == null) {
attrNames.put("#ul", ATTR_USERNAME_LINK_UUID);
updateExpressionBuilder.append(" REMOVE #ul");
}
updateItemRequest = UpdateItemRequest.builder()
.tableName(accountsTableName)
.key(Map.of(KEY_ACCOUNT_UUID, AttributeValues.fromUUID(account.getUuid())))
.updateExpression(updateExpression)
.updateExpression(updateExpressionBuilder.toString())
.conditionExpression("attribute_exists(#number) AND #version = :version")
.expressionAttributeNames(attrNames)
.expressionAttributeValues(attrValues)
@@ -630,11 +645,18 @@ public class Accounts extends AbstractDynamoDbStore {
);
}
@Nonnull
public Optional<Account> getByUsernameLinkHandle(final UUID usernameLinkHandle) {
return requireNonNull(GET_BY_USERNAME_LINK_HANDLE_TIMER.record(() ->
itemByGsiKey(accountsTableName, USERNAME_LINK_TO_UUID_INDEX, ATTR_USERNAME_LINK_UUID, AttributeValues.fromUUID(usernameLinkHandle))
.map(Accounts::fromItem)));
}
@Nonnull
public Optional<Account> getByAccountIdentifier(final UUID uuid) {
return requireNonNull(GET_BY_UUID_TIMER.record(() ->
itemByKey(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))
.map(Accounts::fromItem)));
itemByKey(accountsTableName, KEY_ACCOUNT_UUID, AttributeValues.fromUUID(uuid))
.map(Accounts::fromItem)));
}
public void delete(final UUID uuid) {
@@ -706,6 +728,33 @@ public class Accounts extends AbstractDynamoDbStore {
return Optional.ofNullable(response.item()).filter(m -> !m.isEmpty());
}
@Nonnull
private Optional<Map<String, AttributeValue>> itemByGsiKey(final String table, final String indexName, final String keyName, final AttributeValue keyValue) {
final QueryResponse response = db().query(QueryRequest.builder()
.tableName(table)
.indexName(indexName)
.keyConditionExpression("#gsiKey = :gsiValue")
.projectionExpression("#uuid")
.expressionAttributeNames(Map.of(
"#gsiKey", keyName,
"#uuid", KEY_ACCOUNT_UUID))
.expressionAttributeValues(Map.of(
":gsiValue", keyValue))
.build());
if (response.count() == 0) {
return Optional.empty();
}
if (response.count() > 1) {
throw new IllegalStateException("More than one row located for GSI [%s], key-value pair [%s, %s]"
.formatted(indexName, keyName, keyValue));
}
final AttributeValue primaryKeyValue = response.items().get(0).get(KEY_ACCOUNT_UUID);
return itemByKey(table, KEY_ACCOUNT_UUID, primaryKeyValue);
}
@Nonnull
private TransactWriteItem buildAccountPut(
final Account account,
@@ -854,6 +903,7 @@ public class Accounts extends AbstractDynamoDbStore {
account.setNumber(item.get(ATTR_ACCOUNT_E164).s(), phoneNumberIdentifierFromAttribute);
account.setUuid(accountIdentifier);
account.setUsernameHash(AttributeValues.getByteArray(item, ATTR_USERNAME_HASH, null));
account.setUsernameLinkHandle(AttributeValues.getUUID(item, ATTR_USERNAME_LINK_UUID, null));
account.setVersion(Integer.parseInt(item.get(ATTR_VERSION).n()));
account.setCanonicallyDiscoverable(Optional.ofNullable(item.get(ATTR_CANONICALLY_DISCOVERABLE))
.map(AttributeValue::bool)

View File

@@ -36,7 +36,6 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -63,12 +62,14 @@ public class AccountsManager {
private static final Timer updateTimer = metricRegistry.timer(name(AccountsManager.class, "update"));
private static final Timer getByNumberTimer = metricRegistry.timer(name(AccountsManager.class, "getByNumber"));
private static final Timer getByUsernameHashTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsernameHash"));
private static final Timer getByUsernameLinkHandleTimer = metricRegistry.timer(name(AccountsManager.class, "getByUsernameLinkHandle"));
private static final Timer getByUuidTimer = metricRegistry.timer(name(AccountsManager.class, "getByUuid"));
private static final Timer deleteTimer = metricRegistry.timer(name(AccountsManager.class, "delete"));
private static final Timer redisSetTimer = metricRegistry.timer(name(AccountsManager.class, "redisSet"));
private static final Timer redisNumberGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisNumberGet"));
private static final Timer redisUsernameHashGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameHashGet"));
private static final Timer redisUsernameLinkHandleGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUsernameLinkHandleGet"));
private static final Timer redisPniGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisPniGet"));
private static final Timer redisUuidGetTimer = metricRegistry.timer(name(AccountsManager.class, "redisUuidGet"));
private static final Timer redisDeleteTimer = metricRegistry.timer(name(AccountsManager.class, "redisDelete"));
@@ -620,55 +621,44 @@ public class AccountsManager {
});
}
public Optional<Account> getByE164(String number) {
try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGetByE164(number);
if (account.isEmpty()) {
account = accounts.getByE164(number);
account.ifPresent(this::redisSet);
}
return account;
}
public Optional<Account> getByE164(final String number) {
return checkRedisThenAccounts(
getByNumberTimer,
() -> redisGetBySecondaryKey(getAccountMapKey(number), redisNumberGetTimer),
() -> accounts.getByE164(number)
);
}
public Optional<Account> getByPhoneNumberIdentifier(UUID pni) {
try (Timer.Context ignored = getByNumberTimer.time()) {
Optional<Account> account = redisGetByPhoneNumberIdentifier(pni);
public Optional<Account> getByPhoneNumberIdentifier(final UUID pni) {
return checkRedisThenAccounts(
getByNumberTimer,
() -> redisGetBySecondaryKey(getAccountMapKey(pni.toString()), redisPniGetTimer),
() -> accounts.getByPhoneNumberIdentifier(pni)
);
}
if (account.isEmpty()) {
account = accounts.getByPhoneNumberIdentifier(pni);
account.ifPresent(this::redisSet);
}
return account;
}
public Optional<Account> getByUsernameLinkHandle(final UUID usernameLinkHandle) {
return checkRedisThenAccounts(
getByUsernameLinkHandleTimer,
() -> redisGetBySecondaryKey(getAccountMapKey(usernameLinkHandle.toString()), redisUsernameLinkHandleGetTimer),
() -> accounts.getByUsernameLinkHandle(usernameLinkHandle)
);
}
public Optional<Account> getByUsernameHash(final byte[] usernameHash) {
try (final Timer.Context ignored = getByUsernameHashTimer.time()) {
Optional<Account> account = redisGetByUsernameHash(usernameHash);
if (account.isEmpty()) {
account = accounts.getByUsernameHash(usernameHash);
account.ifPresent(this::redisSet);
}
return account;
}
return checkRedisThenAccounts(
getByUsernameHashTimer,
() -> redisGetBySecondaryKey(getUsernameHashAccountMapKey(usernameHash), redisUsernameHashGetTimer),
() -> accounts.getByUsernameHash(usernameHash)
);
}
public Optional<Account> getByAccountIdentifier(UUID uuid) {
try (Timer.Context ignored = getByUuidTimer.time()) {
Optional<Account> account = redisGetByAccountIdentifier(uuid);
if (account.isEmpty()) {
account = accounts.getByAccountIdentifier(uuid);
account.ifPresent(this::redisSet);
}
return account;
}
public Optional<Account> getByAccountIdentifier(final UUID uuid) {
return checkRedisThenAccounts(
getByUuidTimer,
() -> redisGetByAccountIdentifier(uuid),
() -> accounts.getByAccountIdentifier(uuid)
);
}
public UUID getPhoneNumberIdentifier(String e164) {
@@ -758,24 +748,25 @@ public class AccountsManager {
}
}
private Optional<Account> redisGetByPhoneNumberIdentifier(UUID uuid) {
return redisGetBySecondaryKey(getAccountMapKey(uuid.toString()), redisPniGetTimer);
private Optional<Account> checkRedisThenAccounts(
final Timer overallTimer,
final Supplier<Optional<Account>> resolveFromRedis,
final Supplier<Optional<Account>> resolveFromAccounts) {
try (final Timer.Context ignored = overallTimer.time()) {
Optional<Account> account = resolveFromRedis.get();
if (account.isEmpty()) {
account = resolveFromAccounts.get();
account.ifPresent(this::redisSet);
}
return account;
}
}
private Optional<Account> redisGetByE164(String e164) {
return redisGetBySecondaryKey(getAccountMapKey(e164), redisNumberGetTimer);
}
private Optional<Account> redisGetByUsernameHash(byte[] usernameHash) {
return redisGetBySecondaryKey(getUsernameHashAccountMapKey(usernameHash), redisUsernameHashGetTimer);
}
private Optional<Account> redisGetBySecondaryKey(String secondaryKey, Timer timer) {
try (Timer.Context ignored = timer.time()) {
final String uuid = cacheCluster.withCluster(connection -> connection.sync().get(secondaryKey));
if (uuid != null) return redisGetByAccountIdentifier(UUID.fromString(uuid));
else return Optional.empty();
private Optional<Account> redisGetBySecondaryKey(final String secondaryKey, final Timer timer) {
try (final Timer.Context ignored = timer.time()) {
return Optional.ofNullable(cacheCluster.withCluster(connection -> connection.sync().get(secondaryKey)))
.map(UUID::fromString)
.flatMap(this::getByAccountIdentifier);
} catch (IllegalArgumentException e) {
logger.warn("Deserialization error", e);
return Optional.empty();

View File

@@ -1,10 +1,15 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
public class UsernameHashZkProofVerifier {
public void verifyProof(byte[] proof, byte[] hash) throws BaseUsernameException {
public void verifyProof(final byte[] proof, final byte[] hash) throws BaseUsernameException {
Username.verifyProof(proof, hash);
}
}