Add svr3 share-set store/retrieve

This commit is contained in:
ravi-signal
2024-05-17 10:45:18 -05:00
committed by GitHub
parent 1182d159aa
commit ce1c5be940
18 changed files with 493 additions and 92 deletions

View File

@@ -29,7 +29,7 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
import org.whispersystems.textsecuregcm.entities.AuthCheckResponse;
import org.whispersystems.textsecuregcm.entities.AuthCheckResponseV2;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -100,9 +100,9 @@ public class SecureValueRecovery2Controller {
@ApiResponse(responseCode = "200", description = "`JSON` with the check results.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "422", description = "Provided list of SVR2 credentials could not be parsed")
@ApiResponse(responseCode = "400", description = "`POST` request body is not a valid `JSON`")
public AuthCheckResponse authCheck(@NotNull @Valid final AuthCheckRequest request) {
public AuthCheckResponseV2 authCheck(@NotNull @Valid final AuthCheckRequest request) {
final List<ExternalServiceCredentialsSelector.CredentialInfo> credentials = ExternalServiceCredentialsSelector.check(
request.passwords(),
request.tokens(),
backupServiceCredentialGenerator,
MAX_AGE_SECONDS);
@@ -113,16 +113,16 @@ public class SecureValueRecovery2Controller {
.map(backupServiceCredentialGenerator::generateForUuid)
.map(ExternalServiceCredentials::username);
return new AuthCheckResponse(credentials.stream().collect(Collectors.toMap(
return new AuthCheckResponseV2(credentials.stream().collect(Collectors.toMap(
ExternalServiceCredentialsSelector.CredentialInfo::token,
info -> {
if (!info.valid()) {
return AuthCheckResponse.Result.INVALID;
return AuthCheckResponseV2.Result.INVALID;
}
final String username = info.credentials().username();
// does this credential match the account id for the e164 provided in the request?
boolean match = matchingUsername.filter(username::equals).isPresent();
return match ? AuthCheckResponse.Result.MATCH : AuthCheckResponse.Result.NO_MATCH;
return match ? AuthCheckResponseV2.Result.MATCH : AuthCheckResponseV2.Result.NO_MATCH;
}
)));
}

View File

@@ -10,19 +10,6 @@ import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery3Configuration;
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
import org.whispersystems.textsecuregcm.entities.AuthCheckResponse;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.websocket.auth.ReadOnly;
import java.time.Clock;
import java.util.List;
import java.util.Optional;
@@ -33,9 +20,26 @@ import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery3Configuration;
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
import org.whispersystems.textsecuregcm.entities.AuthCheckResponseV3;
import org.whispersystems.textsecuregcm.entities.SetShareSetRequest;
import org.whispersystems.textsecuregcm.entities.Svr3Credentials;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Optionals;
import org.whispersystems.websocket.auth.Mutable;
import org.whispersystems.websocket.auth.ReadOnly;
@Path("/v3/backup")
@Tag(name = "Secure Value Recovery")
@@ -48,7 +52,8 @@ public class SecureValueRecovery3Controller {
}
@VisibleForTesting
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery3Configuration cfg, final Clock clock) {
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery3Configuration cfg,
final Clock clock) {
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
@@ -62,7 +67,7 @@ public class SecureValueRecovery3Controller {
private final AccountsManager accountsManager;
public SecureValueRecovery3Controller(final ExternalServiceCredentialsGenerator backupServiceCredentialGenerator,
final AccountsManager accountsManager) {
final AccountsManager accountsManager) {
this.backupServiceCredentialGenerator = backupServiceCredentialGenerator;
this.accountsManager = accountsManager;
}
@@ -73,16 +78,36 @@ public class SecureValueRecovery3Controller {
@Operation(
summary = "Generate credentials for SVR3",
description = """
Generate SVR3 service credentials. Generated credentials have an expiration time of 30 days
Generate SVR3 service credentials. Generated credentials have an expiration time of 30 days
(however, the TTL is fully controlled by the server side and may change even for already generated credentials).
"""
)
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
If a share-set has been previously set via /v3/backups/share-set, it will be included in the response
""")
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials and share-set", useReturnTypeSchema = true)
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
public ExternalServiceCredentials getAuth(@ReadOnly @Auth final AuthenticatedAccount auth) {
return backupServiceCredentialGenerator.generateFor(auth.getAccount().getUuid().toString());
public Svr3Credentials getAuth(@ReadOnly @Auth final AuthenticatedAccount auth) {
final ExternalServiceCredentials creds = backupServiceCredentialGenerator.generateFor(
auth.getAccount().getUuid().toString());
return new Svr3Credentials(creds.username(), creds.password(), auth.getAccount().getSvr3ShareSet());
}
@PUT
@Path("/share-set")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Set a share-set for the account",
description = """
Add a share-set to the account that can later be retrieved at v3/backups/auth or during registration. After
storing a value with SVR3, clients must store the returned share-set so the value can be restored later.
""")
@ApiResponse(responseCode = "204", description = "Successfully set share-set")
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
public void setShareSet(
@Mutable @Auth final AuthenticatedAccount auth,
@NotNull @Valid final SetShareSetRequest request) {
accountsManager.update(auth.getAccount(), account -> account.setSvr3ShareSet(request.shareSet()));
}
@POST
@Path("/auth/check")
@@ -92,38 +117,44 @@ public class SecureValueRecovery3Controller {
@Operation(
summary = "Check SVR3 credentials",
description = """
Over time, clients may wind up with multiple sets of SVR3 authentication credentials in cloud storage.
Over time, clients may wind up with multiple sets of SVR3 authentication credentials in cloud storage.
To determine which set is most current and should be used to communicate with SVR3 to retrieve a master key
(from which a registration recovery password can be derived), clients should call this endpoint
with a list of stored credentials. The response will identify which (if any) set of credentials are appropriate for communicating with SVR3.
"""
)
(from which a registration recovery password can be derived), clients should call this endpoint
with a list of stored credentials. The response will identify which (if any) set of credentials are
appropriate for communicating with SVR3.
""")
@ApiResponse(responseCode = "200", description = "`JSON` with the check results.", useReturnTypeSchema = true)
@ApiResponse(responseCode = "422", description = "Provided list of SVR3 credentials could not be parsed")
@ApiResponse(responseCode = "400", description = "`POST` request body is not a valid `JSON`")
public AuthCheckResponse authCheck(@NotNull @Valid final AuthCheckRequest request) {
public AuthCheckResponseV3 authCheck(@NotNull @Valid final AuthCheckRequest request) {
final List<ExternalServiceCredentialsSelector.CredentialInfo> credentials = ExternalServiceCredentialsSelector.check(
request.passwords(),
request.tokens(),
backupServiceCredentialGenerator,
MAX_AGE_SECONDS);
final Optional<Account> account = accountsManager.getByE164(request.number());
// the username associated with the provided number
final Optional<String> matchingUsername = accountsManager
.getByE164(request.number())
final Optional<String> matchingUsername = account
.map(Account::getUuid)
.map(backupServiceCredentialGenerator::generateForUuid)
.map(ExternalServiceCredentials::username);
return new AuthCheckResponse(credentials.stream().collect(Collectors.toMap(
return new AuthCheckResponseV3(credentials.stream().collect(Collectors.toMap(
ExternalServiceCredentialsSelector.CredentialInfo::token,
info -> {
if (!info.valid()) {
return AuthCheckResponse.Result.INVALID;
// This isn't a valid credential (could be for a different SVR service, expired, etc)
return AuthCheckResponseV3.Result.invalid();
}
final String username = info.credentials().username();
// does this credential match the account id for the e164 provided in the request?
boolean match = matchingUsername.filter(username::equals).isPresent();
return match ? AuthCheckResponse.Result.MATCH : AuthCheckResponse.Result.NO_MATCH;
final String credUsername = info.credentials().username();
return Optionals
// If the account exists, and the account's username matches this credential's username, return a match
.zipWith(account, matchingUsername.filter(credUsername::equals), (a, ignored) ->
AuthCheckResponseV3.Result.match(a.getSvr3ShareSet()))
// Otherwise, return no-match
.orElseGet(AuthCheckResponseV3.Result::noMatch);
}
)));
}