gRPC API for external services credentials service

This commit is contained in:
Sergey Skrobotov
2023-09-14 14:38:58 -07:00
parent d0fdae3df7
commit 0b3af7d824
13 changed files with 698 additions and 15 deletions

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import java.time.Clock;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.signal.chat.credentials.AuthCheckResult;
import org.signal.chat.credentials.CheckSvrCredentialsRequest;
import org.signal.chat.credentials.CheckSvrCredentialsResponse;
import org.signal.chat.credentials.ReactorExternalServiceCredentialsAnonymousGrpc;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class ExternalServiceCredentialsAnonymousGrpcService extends
ReactorExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousImplBase {
private static final long MAX_SVR_PASSWORD_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30);
private final ExternalServiceCredentialsGenerator svrCredentialsGenerator;
private final AccountsManager accountsManager;
public static ExternalServiceCredentialsAnonymousGrpcService create(
final AccountsManager accountsManager,
final WhisperServerConfiguration chatConfiguration) {
return new ExternalServiceCredentialsAnonymousGrpcService(
accountsManager,
ExternalServiceDefinitions.SVR.generatorFactory().apply(chatConfiguration, Clock.systemUTC())
);
}
@VisibleForTesting
ExternalServiceCredentialsAnonymousGrpcService(
final AccountsManager accountsManager,
final ExternalServiceCredentialsGenerator svrCredentialsGenerator) {
this.accountsManager = requireNonNull(accountsManager);
this.svrCredentialsGenerator = requireNonNull(svrCredentialsGenerator);
}
@Override
public Mono<CheckSvrCredentialsResponse> checkSvrCredentials(final CheckSvrCredentialsRequest request) {
final List<String> tokens = request.getPasswordsList();
final List<ExternalServiceCredentialsSelector.CredentialInfo> credentials = ExternalServiceCredentialsSelector.check(
tokens,
svrCredentialsGenerator,
MAX_SVR_PASSWORD_AGE_SECONDS);
// the username associated with the provided number
final Optional<String> matchingUsername = accountsManager
.getByE164(request.getNumber())
.map(Account::getUuid)
.map(svrCredentialsGenerator::generateForUuid)
.map(ExternalServiceCredentials::username);
return Flux.fromIterable(credentials)
.reduce(CheckSvrCredentialsResponse.newBuilder(), ((builder, credentialInfo) -> {
final AuthCheckResult authCheckResult;
if (!credentialInfo.valid()) {
authCheckResult = AuthCheckResult.AUTH_CHECK_RESULT_INVALID;
} else {
final String username = credentialInfo.credentials().username();
// does this credential match the account id for the e164 provided in the request?
authCheckResult = matchingUsername.map(username::equals).orElse(false)
? AuthCheckResult.AUTH_CHECK_RESULT_MATCH
: AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH;
}
return builder.putMatches(credentialInfo.token(), authCheckResult);
}))
.map(CheckSvrCredentialsResponse.Builder::build);
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import io.grpc.Status;
import java.time.Clock;
import java.util.Map;
import org.signal.chat.credentials.ExternalServiceType;
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
import org.signal.chat.credentials.ReactorExternalServiceCredentialsGrpc;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import reactor.core.publisher.Mono;
public class ExternalServiceCredentialsGrpcService extends ReactorExternalServiceCredentialsGrpc.ExternalServiceCredentialsImplBase {
private final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType;
private final RateLimiters rateLimiters;
public static ExternalServiceCredentialsGrpcService createForAllExternalServices(
final WhisperServerConfiguration chatConfiguration,
final RateLimiters rateLimiters) {
return new ExternalServiceCredentialsGrpcService(
ExternalServiceDefinitions.createExternalServiceList(chatConfiguration, Clock.systemUTC()),
rateLimiters
);
}
@VisibleForTesting
ExternalServiceCredentialsGrpcService(
final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType,
final RateLimiters rateLimiters) {
this.credentialsGeneratorByType = requireNonNull(credentialsGeneratorByType);
this.rateLimiters = requireNonNull(rateLimiters);
}
@Override
public Mono<GetExternalServiceCredentialsResponse> getExternalServiceCredentials(final GetExternalServiceCredentialsRequest request) {
final ExternalServiceCredentialsGenerator credentialsGenerator = this.credentialsGeneratorByType
.get(request.getExternalService());
if (credentialsGenerator == null) {
return Mono.error(Status.INVALID_ARGUMENT.asException());
}
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
return rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validateReactive(authenticatedDevice.accountIdentifier())
.then(Mono.fromSupplier(() -> {
final ExternalServiceCredentials externalServiceCredentials = credentialsGenerator
.generateForUuid(authenticatedDevice.accountIdentifier());
return GetExternalServiceCredentialsResponse.newBuilder()
.setUsername(externalServiceCredentials.username())
.setPassword(externalServiceCredentials.password())
.build();
}));
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import static java.util.Objects.requireNonNull;
import java.time.Clock;
import java.util.Arrays;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.signal.chat.credentials.ExternalServiceType;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
enum ExternalServiceDefinitions {
ART(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, (chatConfig, clock) -> {
final ArtServiceConfiguration cfg = chatConfig.getArtServiceConfiguration();
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.withUserDerivationKey(cfg.userAuthenticationTokenUserIdSecret())
.prependUsername(false)
.truncateSignature(false)
.build();
}),
DIRECTORY(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, (chatConfig, clock) -> {
final DirectoryV2ClientConfiguration cfg = chatConfig.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration();
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.withUserDerivationKey(cfg.userIdTokenSharedSecret())
.prependUsername(false)
.withClock(clock)
.build();
}),
PAYMENTS(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, (chatConfig, clock) -> {
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.prependUsername(true)
.build();
}),
SVR(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVR, (chatConfig, clock) -> {
final SecureValueRecovery2Configuration cfg = chatConfig.getSvr2Configuration();
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
.prependUsername(false)
.withDerivedUsernameTruncateLength(16)
.withClock(clock)
.build();
}),
STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> {
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
return ExternalServiceCredentialsGenerator
.builder(cfg.userAuthenticationTokenSharedSecret())
.prependUsername(true)
.build();
}),
;
private final ExternalServiceType externalService;
private final BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> generatorFactory;
ExternalServiceDefinitions(
final ExternalServiceType externalService,
final BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> factory) {
this.externalService = requireNonNull(externalService);
this.generatorFactory = requireNonNull(factory);
}
public static Map<ExternalServiceType, ExternalServiceCredentialsGenerator> createExternalServiceList(
final WhisperServerConfiguration chatConfiguration,
final Clock clock) {
return Arrays.stream(values())
.map(esd -> Pair.of(esd.externalService, esd.generatorFactory().apply(chatConfiguration, clock)))
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
}
public BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> generatorFactory() {
return generatorFactory;
}
ExternalServiceType externalService() {
return externalService;
}
}