mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 10:48:04 +01:00
username links API
This commit is contained in:
@@ -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 {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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))),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user