Compare commits
No commits in common. "main" and "v20250709.0.0" have entirely different histories.
main
...
v20250709.
|
@ -16,9 +16,6 @@ directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789
|
|||
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
|
||||
svrb.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth tokens for Signal users
|
||||
svrb.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth identity tokens for Signal users
|
||||
|
||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||
|
||||
gcpAttachments.rsaSigningKey: |
|
||||
|
|
|
@ -225,34 +225,6 @@ svr2:
|
|||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
svrb:
|
||||
uri: svrb.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://svrb.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://svrb.userIdTokenSharedSecret
|
||||
svrCaCertificates:
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes: 1
|
||||
cluster:
|
||||
|
|
|
@ -52,7 +52,7 @@ import org.whispersystems.textsecuregcm.configuration.RemoteConfigConfiguration;
|
|||
import org.whispersystems.textsecuregcm.configuration.ReportMessageConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.S3ObjectMonitorFactory;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.configuration.ShortCodeExpanderConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SpamFilterConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.StripeConfiguration;
|
||||
|
@ -156,12 +156,7 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private SecureValueRecoveryConfiguration svr2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonProperty
|
||||
private SecureValueRecoveryConfiguration svrb;
|
||||
private SecureValueRecovery2Configuration svr2;
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
|
@ -394,14 +389,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return pubsub;
|
||||
}
|
||||
|
||||
public SecureValueRecoveryConfiguration getSvr2Configuration() {
|
||||
public SecureValueRecovery2Configuration getSvr2Configuration() {
|
||||
return svr2;
|
||||
}
|
||||
|
||||
public SecureValueRecoveryConfiguration getSvrbConfiguration() {
|
||||
return svrb;
|
||||
}
|
||||
|
||||
public DirectoryV2Configuration getDirectoryV2Configuration() {
|
||||
return directoryV2;
|
||||
}
|
||||
|
|
|
@ -124,7 +124,6 @@ import org.whispersystems.textsecuregcm.controllers.RegistrationController;
|
|||
import org.whispersystems.textsecuregcm.controllers.RemoteConfigController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
|
||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecoveryBController;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
|
||||
import org.whispersystems.textsecuregcm.controllers.VerificationController;
|
||||
|
@ -280,7 +279,6 @@ import org.whispersystems.textsecuregcm.workers.RemoveExpiredAccountsCommand;
|
|||
import org.whispersystems.textsecuregcm.workers.RemoveExpiredBackupsCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.RemoveExpiredLinkedDevicesCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.RemoveExpiredUsernameHoldsCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.RemoveOrphanedPreKeyPagesCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||
|
@ -334,7 +332,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
bootstrap.addCommand(new RemoveExpiredAccountsCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new RemoveExpiredUsernameHoldsCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new RemoveExpiredBackupsCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new RemoveOrphanedPreKeyPagesCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new BackupMetricsCommand(Clock.systemUTC()));
|
||||
bootstrap.addCommand(new BackupUsageRecalculationCommand());
|
||||
bootstrap.addCommand(new RemoveExpiredLinkedDevicesCommand());
|
||||
|
@ -609,8 +606,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
config.getPaymentsServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
||||
config.getSvr2Configuration());
|
||||
ExternalServiceCredentialsGenerator svrbCredentialsGenerator = SecureValueRecoveryBController.credentialsGenerator(
|
||||
config.getSvrbConfiguration());
|
||||
|
||||
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
|
||||
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
|
||||
|
@ -1124,7 +1119,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||
new SecureValueRecoveryBController(svrbCredentialsGenerator),
|
||||
new StickerController(rateLimiters, config.getCdnConfiguration().credentials().accessKeyId().value(),
|
||||
config.getCdnConfiguration().credentials().secretAccessKey().value(), config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket()),
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.util.List;
|
|||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
|
||||
public record SecureValueRecoveryConfiguration(
|
||||
public record SecureValueRecovery2Configuration(
|
||||
@NotBlank String uri,
|
||||
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
|
||||
@ExactlySize(32) SecretBytes userIdTokenSharedSecret,
|
||||
|
@ -20,7 +20,7 @@ public record SecureValueRecoveryConfiguration(
|
|||
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
|
||||
@NotNull @Valid RetryConfiguration retry) {
|
||||
|
||||
public SecureValueRecoveryConfiguration {
|
||||
public SecureValueRecovery2Configuration {
|
||||
if (circuitBreaker == null) {
|
||||
circuitBreaker = new CircuitBreakerConfiguration();
|
||||
}
|
|
@ -8,7 +8,6 @@ package org.whispersystems.textsecuregcm.controllers;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
|
@ -28,7 +27,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
|||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.AuthCheckResponseV2;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
|
||||
|
@ -36,19 +35,18 @@ import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
|||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
|
||||
@Path("/v2/{name: backup|svr}")
|
||||
@Path("/v2/backup")
|
||||
@Tag(name = "Secure Value Recovery")
|
||||
@Schema(description = "Note: /v2/backup is deprecated. Use /v2/svr instead.")
|
||||
public class SecureValueRecovery2Controller {
|
||||
|
||||
private static final long MAX_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30);
|
||||
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg) {
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery2Configuration cfg) {
|
||||
return credentialsGenerator(cfg, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg, final Clock clock) {
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecovery2Configuration cfg, final Clock clock) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
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 jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.time.Clock;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
|
||||
@Path("/v1/svrb")
|
||||
@Tag(name = "Secure Value Recovery B")
|
||||
public class SecureValueRecoveryBController {
|
||||
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg) {
|
||||
return credentialsGenerator(cfg, Clock.systemUTC());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg,
|
||||
final Clock clock) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
|
||||
.prependUsername(false)
|
||||
.withDerivedUsernameTruncateLength(16)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}
|
||||
|
||||
private final ExternalServiceCredentialsGenerator svrbCredentialGenerator;
|
||||
|
||||
public SecureValueRecoveryBController(final ExternalServiceCredentialsGenerator svrbCredentialGenerator) {
|
||||
this.svrbCredentialGenerator = svrbCredentialGenerator;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "Generate credentials for SVRB",
|
||||
description = """
|
||||
Generate SVRB service credentials. Generated credentials have an expiration time of 1 day (subject to change)
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
|
||||
public ExternalServiceCredentials getAuth(@Auth final AuthenticatedDevice auth) {
|
||||
return svrbCredentialGenerator.generateFor(auth.accountIdentifier().toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
|
||||
public record AuthCheckResponseV3(
|
||||
@Schema(description = """
|
||||
A dictionary with the auth check results, keyed by the token corresponding token provided in the request.
|
||||
""")
|
||||
@NotNull Map<String, Result> matches) {
|
||||
|
||||
public record Result(
|
||||
@Schema(description = "The status of the credential. Either match, no-match, or invalid")
|
||||
CredentialStatus status,
|
||||
|
||||
@Schema(description = """
|
||||
If the credential was a match, the stored shareSet that can be used to restore a value from SVR. Encoded in
|
||||
standard un-padded base64.
|
||||
""", implementation = String.class)
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@Nullable byte[] shareSet) {
|
||||
|
||||
public static Result invalid() {
|
||||
return new Result(CredentialStatus.INVALID, null);
|
||||
}
|
||||
|
||||
public static Result noMatch() {
|
||||
return new Result(CredentialStatus.NO_MATCH, null);
|
||||
}
|
||||
|
||||
public static Result match(@Nullable final byte[] shareSet) {
|
||||
return new Result(CredentialStatus.MATCH, shareSet);
|
||||
}
|
||||
}
|
||||
|
||||
public enum CredentialStatus {
|
||||
MATCH("match"),
|
||||
NO_MATCH("no-match"),
|
||||
INVALID("invalid");
|
||||
|
||||
private final String clientCode;
|
||||
|
||||
CredentialStatus(final String clientCode) {
|
||||
this.clientCode = clientCode;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String clientCode() {
|
||||
return clientCode;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
|||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
|
||||
enum ExternalServiceDefinitions {
|
||||
DIRECTORY(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, (chatConfig, clock) -> {
|
||||
|
@ -38,17 +38,7 @@ enum ExternalServiceDefinitions {
|
|||
.build();
|
||||
}),
|
||||
SVR(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVR, (chatConfig, clock) -> {
|
||||
final SecureValueRecoveryConfiguration cfg = chatConfig.getSvr2Configuration();
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
|
||||
.prependUsername(false)
|
||||
.withDerivedUsernameTruncateLength(16)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
}),
|
||||
SVRB(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVRB, (chatConfig, clock) -> {
|
||||
final SecureValueRecoveryConfiguration cfg = chatConfig.getSvrbConfiguration();
|
||||
final SecureValueRecovery2Configuration cfg = chatConfig.getSvr2Configuration();
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
|
||||
|
|
|
@ -21,7 +21,7 @@ import java.util.concurrent.Executor;
|
|||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient;
|
||||
import org.whispersystems.textsecuregcm.util.HttpUtils;
|
||||
|
||||
|
@ -39,7 +39,7 @@ public class SecureValueRecovery2Client {
|
|||
|
||||
public SecureValueRecovery2Client(final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator,
|
||||
final Executor executor, final ScheduledExecutorService retryExecutor,
|
||||
final SecureValueRecoveryConfiguration configuration)
|
||||
final SecureValueRecovery2Configuration configuration)
|
||||
throws CertificateException {
|
||||
this.secureValueRecoveryCredentialsGenerator = secureValueRecoveryCredentialsGenerator;
|
||||
this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH);
|
||||
|
|
|
@ -53,7 +53,6 @@ enum ExternalServiceType {
|
|||
EXTERNAL_SERVICE_TYPE_PAYMENTS = 2;
|
||||
EXTERNAL_SERVICE_TYPE_STORAGE = 3;
|
||||
EXTERNAL_SERVICE_TYPE_SVR = 4;
|
||||
EXTERNAL_SERVICE_TYPE_SVRB = 5;
|
||||
}
|
||||
|
||||
message GetExternalServiceCredentialsRequest {
|
||||
|
|
|
@ -6,43 +6,28 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
import org.whispersystems.textsecuregcm.entities.AuthCheckResponseV2;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
public class SecureValueRecovery2ControllerTest {
|
||||
public class SecureValueRecovery2ControllerTest extends SecureValueRecoveryControllerBaseTest {
|
||||
|
||||
private static final SecureValueRecoveryConfiguration CFG = new SecureValueRecoveryConfiguration(
|
||||
private static final SecureValueRecovery2Configuration CFG = new SecureValueRecovery2Configuration(
|
||||
"",
|
||||
randomSecretBytes(32),
|
||||
randomSecretBytes(32),
|
||||
|
@ -67,46 +52,12 @@ public class SecureValueRecovery2ControllerTest {
|
|||
.addResource(CONTROLLER)
|
||||
.build();
|
||||
|
||||
@Nested
|
||||
class WithBackupsPrefix extends SecureValueRecoveryControllerBaseTest {
|
||||
protected WithBackupsPrefix() {
|
||||
super("/v2/backup");
|
||||
}
|
||||
protected SecureValueRecovery2ControllerTest() {
|
||||
super("/v2", ACCOUNTS_MANAGER, CLOCK, RESOURCES, CREDENTIAL_GENERATOR);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithSvr2Prefix extends SecureValueRecoveryControllerBaseTest {
|
||||
protected WithSvr2Prefix() {
|
||||
super("/v2/svr");
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class SecureValueRecoveryControllerBaseTest {
|
||||
private static final UUID USER_1 = UUID.randomUUID();
|
||||
private static final UUID USER_2 = UUID.randomUUID();
|
||||
private static final UUID USER_3 = UUID.randomUUID();
|
||||
private static final String E164_VALID = "+18005550123";
|
||||
private static final String E164_INVALID = "1(800)555-0123";
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
Mockito.reset(ACCOUNTS_MANAGER);
|
||||
Mockito.when(ACCOUNTS_MANAGER.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1)));
|
||||
}
|
||||
|
||||
protected SecureValueRecoveryControllerBaseTest(final String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
enum CheckStatus {
|
||||
MATCH,
|
||||
NO_MATCH,
|
||||
INVALID
|
||||
}
|
||||
|
||||
private Map<String, CheckStatus> parseCheckResponse(final Response response) {
|
||||
@Override
|
||||
Map<String, CheckStatus> parseCheckResponse(final Response response) {
|
||||
final AuthCheckResponseV2 authCheckResponseV2 = response.readEntity(AuthCheckResponseV2.class);
|
||||
return authCheckResponseV2.matches().entrySet().stream().collect(Collectors.toMap(
|
||||
Map.Entry::getKey, e -> switch (e.getValue()) {
|
||||
|
@ -116,256 +67,4 @@ public class SecureValueRecovery2ControllerTest {
|
|||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneMatch() {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(1)), CheckStatus.MATCH,
|
||||
token(USER_2, day(1)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(1)), CheckStatus.NO_MATCH
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMatch() {
|
||||
validate(Map.of(
|
||||
token(USER_2, day(1)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(1)), CheckStatus.NO_MATCH
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeInvalid() {
|
||||
final ExternalServiceCredentials user1Cred = credentials(USER_1, day(1));
|
||||
final ExternalServiceCredentials user2Cred = credentials(USER_2, day(1));
|
||||
final ExternalServiceCredentials user3Cred = credentials(USER_3, day(1));
|
||||
|
||||
final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password()));
|
||||
validate(Map.of(
|
||||
token(user1Cred), CheckStatus.MATCH,
|
||||
token(user2Cred), CheckStatus.NO_MATCH,
|
||||
fakeToken, CheckStatus.INVALID
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeExpired() {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(100)), CheckStatus.MATCH,
|
||||
token(USER_2, day(100)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID,
|
||||
token(USER_3, day(20)), CheckStatus.INVALID
|
||||
), day(110));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeHaveNewerVersions() {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(10)), CheckStatus.INVALID,
|
||||
token(USER_1, day(20)), CheckStatus.MATCH,
|
||||
token(USER_2, day(10)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(20)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID
|
||||
), day(25));
|
||||
}
|
||||
|
||||
private void validate(
|
||||
final Map<String, CheckStatus> expected,
|
||||
final long nowMillis) {
|
||||
CLOCK.setTimeMillis(nowMillis);
|
||||
final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
|
||||
final Response response = RESOURCES.getJerseyTest().target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(request, MediaType.APPLICATION_JSON));
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
final Map<String, CheckStatus> res = parseCheckResponse(response);
|
||||
assertEquals(expected, res);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeSuccess() {
|
||||
final Map<String, CheckStatus> expected = Map.of(
|
||||
token(USER_1, day(10)), CheckStatus.INVALID,
|
||||
token(USER_1, day(20)), CheckStatus.MATCH,
|
||||
token(USER_2, day(10)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(20)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID
|
||||
);
|
||||
|
||||
CLOCK.setTimeMillis(day(25));
|
||||
|
||||
final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
|
||||
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals(expected, parseCheckResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenInvalidNumber() {
|
||||
final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1"));
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenTooManyTokens() {
|
||||
final AuthCheckRequest inOkay = new AuthCheckRequest(E164_VALID, List.of(
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"
|
||||
));
|
||||
final AuthCheckRequest inTooMany = new AuthCheckRequest(E164_VALID, List.of(
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
|
||||
));
|
||||
final AuthCheckRequest inNoTokens = new AuthCheckRequest(E164_VALID, Collections.emptyList());
|
||||
|
||||
final Response responseOkay = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inOkay, MediaType.APPLICATION_JSON));
|
||||
|
||||
final Response responseError1 = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON));
|
||||
|
||||
final Response responseError2 = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inNoTokens, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (responseOkay; responseError1; responseError2) {
|
||||
assertEquals(200, responseOkay.getStatus());
|
||||
assertEquals(422, responseError1.getStatus());
|
||||
assertEquals(422, responseError2.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenPasswordsMissing() {
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "123"
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenNumberMissing() {
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"passwords": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenExtraFields() {
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"passwords": ["aaa:bbb"],
|
||||
"unexpected": "value"
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptsPasswordsOrTokens() {
|
||||
final Response passwordsResponse = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"passwords": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
try (passwordsResponse) {
|
||||
assertEquals(200, passwordsResponse.getStatus());
|
||||
}
|
||||
|
||||
final Response tokensResponse = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"tokens": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
try (tokensResponse) {
|
||||
assertEquals(200, tokensResponse.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenNotAJson() {
|
||||
final Response response = RESOURCES.getJerseyTest()
|
||||
.target(pathPrefix + "/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("random text", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(400, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private String token(final UUID uuid, final long timeMillis) {
|
||||
return token(credentials(uuid, timeMillis));
|
||||
}
|
||||
|
||||
private static String token(final ExternalServiceCredentials credentials) {
|
||||
return credentials.username() + ":" + credentials.password();
|
||||
}
|
||||
|
||||
private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) {
|
||||
CLOCK.setTimeMillis(timeMillis);
|
||||
return CREDENTIAL_GENERATOR.generateForUuid(uuid);
|
||||
}
|
||||
|
||||
private static long day(final int n) {
|
||||
return TimeUnit.DAYS.toMillis(n);
|
||||
}
|
||||
|
||||
private static Account account(final UUID uuid) {
|
||||
final Account a = new Account();
|
||||
a.setUuid(uuid);
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
import java.time.Instant;
|
||||
import java.util.HexFormat;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
public class SecureValueRecoveryBControllerTest {
|
||||
|
||||
private static final SecureValueRecoveryConfiguration CFG = new SecureValueRecoveryConfiguration(
|
||||
"",
|
||||
randomSecretBytes(32),
|
||||
randomSecretBytes(32),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
private static final MutableClock CLOCK = new MutableClock();
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator CREDENTIAL_GENERATOR =
|
||||
SecureValueRecoveryBController.credentialsGenerator(CFG, CLOCK);
|
||||
|
||||
private static final SecureValueRecoveryBController CONTROLLER =
|
||||
new SecureValueRecoveryBController(CREDENTIAL_GENERATOR);
|
||||
|
||||
private static final ResourceExtension RESOURCES = ResourceExtension.builder()
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(CONTROLLER)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testGetCredentials() {
|
||||
CLOCK.setTimeInstant(Instant.ofEpochSecond(123));
|
||||
final ExternalServiceCredentials creds = RESOURCES.getJerseyTest()
|
||||
.target("/v1/svrb/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(HexFormat.of().parseHex(creds.username())).hasSize(16);
|
||||
System.out.println(creds.password());
|
||||
final String[] split = creds.password().split(":", 2);
|
||||
assertThat(Long.parseLong(split[0])).isEqualTo(123);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.entities.AuthCheckRequest;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
abstract class SecureValueRecoveryControllerBaseTest {
|
||||
|
||||
private static final UUID USER_1 = UUID.randomUUID();
|
||||
|
||||
private static final UUID USER_2 = UUID.randomUUID();
|
||||
|
||||
private static final UUID USER_3 = UUID.randomUUID();
|
||||
|
||||
private static final String E164_VALID = "+18005550123";
|
||||
|
||||
private static final String E164_INVALID = "1(800)555-0123";
|
||||
|
||||
private final String pathPrefix;
|
||||
private final ResourceExtension resourceExtension;
|
||||
private final AccountsManager mockAccountsManager;
|
||||
private final ExternalServiceCredentialsGenerator credentialsGenerator;
|
||||
private final MutableClock clock;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
Mockito.when(mockAccountsManager.getByE164(E164_VALID)).thenReturn(Optional.of(account(USER_1)));
|
||||
}
|
||||
|
||||
protected SecureValueRecoveryControllerBaseTest(
|
||||
final String pathPrefix,
|
||||
final AccountsManager mockAccountsManager,
|
||||
final MutableClock mutableClock,
|
||||
final ResourceExtension resourceExtension,
|
||||
final ExternalServiceCredentialsGenerator credentialsGenerator) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
this.resourceExtension = resourceExtension;
|
||||
this.mockAccountsManager = mockAccountsManager;
|
||||
this.credentialsGenerator = credentialsGenerator;
|
||||
this.clock = mutableClock;
|
||||
}
|
||||
|
||||
enum CheckStatus {
|
||||
MATCH,
|
||||
NO_MATCH,
|
||||
INVALID
|
||||
}
|
||||
abstract Map<String, CheckStatus> parseCheckResponse(Response response);
|
||||
|
||||
@Test
|
||||
public void testOneMatch() throws Exception {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(1)), CheckStatus.MATCH,
|
||||
token(USER_2, day(1)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(1)), CheckStatus.NO_MATCH
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoMatch() throws Exception {
|
||||
validate(Map.of(
|
||||
token(USER_2, day(1)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(1)), CheckStatus.NO_MATCH
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeInvalid() throws Exception {
|
||||
final ExternalServiceCredentials user1Cred = credentials(USER_1, day(1));
|
||||
final ExternalServiceCredentials user2Cred = credentials(USER_2, day(1));
|
||||
final ExternalServiceCredentials user3Cred = credentials(USER_3, day(1));
|
||||
|
||||
final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password()));
|
||||
validate(Map.of(
|
||||
token(user1Cred), CheckStatus.MATCH,
|
||||
token(user2Cred), CheckStatus.NO_MATCH,
|
||||
fakeToken, CheckStatus.INVALID
|
||||
), day(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeExpired() throws Exception {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(100)), CheckStatus.MATCH,
|
||||
token(USER_2, day(100)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID,
|
||||
token(USER_3, day(20)), CheckStatus.INVALID
|
||||
), day(110));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSomeHaveNewerVersions() throws Exception {
|
||||
validate(Map.of(
|
||||
token(USER_1, day(10)), CheckStatus.INVALID,
|
||||
token(USER_1, day(20)), CheckStatus.MATCH,
|
||||
token(USER_2, day(10)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(20)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID
|
||||
), day(25));
|
||||
}
|
||||
|
||||
private void validate(
|
||||
final Map<String, CheckStatus> expected,
|
||||
final long nowMillis) throws Exception {
|
||||
clock.setTimeMillis(nowMillis);
|
||||
final AuthCheckRequest request = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
|
||||
final Response response = resourceExtension.getJerseyTest().target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(request, MediaType.APPLICATION_JSON));
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
final Map<String, CheckStatus> res = parseCheckResponse(response);
|
||||
assertEquals(expected, res);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeSuccess() throws Exception {
|
||||
final Map<String, CheckStatus> expected = Map.of(
|
||||
token(USER_1, day(10)), CheckStatus.INVALID,
|
||||
token(USER_1, day(20)), CheckStatus.MATCH,
|
||||
token(USER_2, day(10)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(20)), CheckStatus.NO_MATCH,
|
||||
token(USER_3, day(10)), CheckStatus.INVALID
|
||||
);
|
||||
|
||||
clock.setTimeMillis(day(25));
|
||||
|
||||
final AuthCheckRequest in = new AuthCheckRequest(E164_VALID, List.copyOf(expected.keySet()));
|
||||
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals(expected, parseCheckResponse(response));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenInvalidNumber() throws Exception {
|
||||
final AuthCheckRequest in = new AuthCheckRequest(E164_INVALID, Collections.singletonList("1"));
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(in, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenTooManyTokens() throws Exception {
|
||||
final AuthCheckRequest inOkay = new AuthCheckRequest(E164_VALID, List.of(
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"
|
||||
));
|
||||
final AuthCheckRequest inTooMany = new AuthCheckRequest(E164_VALID, List.of(
|
||||
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"
|
||||
));
|
||||
final AuthCheckRequest inNoTokens = new AuthCheckRequest(E164_VALID, Collections.emptyList());
|
||||
|
||||
final Response responseOkay = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inOkay, MediaType.APPLICATION_JSON));
|
||||
|
||||
final Response responseError1 = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inTooMany, MediaType.APPLICATION_JSON));
|
||||
|
||||
final Response responseError2 = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity(inNoTokens, MediaType.APPLICATION_JSON));
|
||||
|
||||
try (responseOkay; responseError1; responseError2) {
|
||||
assertEquals(200, responseOkay.getStatus());
|
||||
assertEquals(422, responseError1.getStatus());
|
||||
assertEquals(422, responseError2.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenPasswordsMissing() throws Exception {
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "123"
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenNumberMissing() throws Exception {
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"passwords": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(422, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenExtraFields() throws Exception {
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"passwords": ["aaa:bbb"],
|
||||
"unexpected": "value"
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(200, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptsPasswordsOrTokens() {
|
||||
final Response passwordsResponse = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"passwords": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
try (passwordsResponse) {
|
||||
assertEquals(200, passwordsResponse.getStatus());
|
||||
}
|
||||
|
||||
final Response tokensResponse = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("""
|
||||
{
|
||||
"number": "+18005550123",
|
||||
"tokens": ["aaa:bbb"]
|
||||
}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
try (tokensResponse) {
|
||||
assertEquals(200, tokensResponse.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpResponseCodeWhenNotAJson() throws Exception {
|
||||
final Response response = resourceExtension.getJerseyTest()
|
||||
.target(pathPrefix + "/backup/auth/check")
|
||||
.request()
|
||||
.post(Entity.entity("random text", MediaType.APPLICATION_JSON));
|
||||
|
||||
try (response) {
|
||||
assertEquals(400, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
String token(final UUID uuid, final long timeMillis) {
|
||||
return token(credentials(uuid, timeMillis));
|
||||
}
|
||||
|
||||
static String token(final ExternalServiceCredentials credentials) {
|
||||
return credentials.username() + ":" + credentials.password();
|
||||
}
|
||||
|
||||
private ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) {
|
||||
clock.setTimeMillis(timeMillis);
|
||||
return credentialsGenerator.generateForUuid(uuid);
|
||||
}
|
||||
|
||||
static long day(final int n) {
|
||||
return TimeUnit.DAYS.toMillis(n);
|
||||
}
|
||||
|
||||
private static Account account(final UUID uuid) {
|
||||
final Account a = new Account();
|
||||
a.setUuid(uuid);
|
||||
return a;
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||
|
||||
class SecureValueRecovery2ClientTest {
|
||||
|
||||
|
@ -55,7 +55,7 @@ class SecureValueRecovery2ClientTest {
|
|||
httpExecutor = Executors.newSingleThreadExecutor();
|
||||
retryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
final SecureValueRecoveryConfiguration config = new SecureValueRecoveryConfiguration(
|
||||
final SecureValueRecovery2Configuration config = new SecureValueRecovery2Configuration(
|
||||
"http://localhost:" + wireMock.getPort(),
|
||||
randomSecretBytes(32),
|
||||
randomSecretBytes(32),
|
||||
|
|
|
@ -53,9 +53,6 @@ directoryV2.client.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789
|
|||
svr2.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth tokens for Signal users
|
||||
svr2.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVR2 to generate auth identity tokens for Signal users
|
||||
|
||||
svrb.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth tokens for Signal users
|
||||
svrb.userIdTokenSharedSecret: bbcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with SVRB to generate auth identity tokens for Signal users
|
||||
|
||||
tus.userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG=
|
||||
|
||||
# The below private key was key generated exclusively for testing purposes. Do not use it in any other context.
|
||||
|
|
|
@ -223,35 +223,6 @@ svr2:
|
|||
9Kxq0DY7RCEpdHMCKcOL
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
svrb:
|
||||
uri: svrb.example.com
|
||||
userAuthenticationTokenSharedSecret: secret://svrb.userAuthenticationTokenSharedSecret
|
||||
userIdTokenSharedSecret: secret://svrb.userIdTokenSharedSecret
|
||||
svrCaCertificates:
|
||||
# this is a randomly generated test certificate
|
||||
- |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDazCCAlOgAwIBAgIUW5lcNWkuynRVc8Rq5pO6mHQBuZAwDQYJKoZIhvcNAQEL
|
||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAzMjUwMzE4MTNaFw0yOTAz
|
||||
MjQwMzE4MTNaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCfH4Um+fv2r4KudhD37/UXp8duRLTmp4XvpBTpDHpD
|
||||
2HF8p2yThVKlJnMkP/9Ey1Rb0vhxO7DCltLdW8IYcxJuHoyMvyhGUEtxxkOZbrk8
|
||||
ciUR9jTZ37x7vXRGj/RxcdlS6iD0MeF0D/LAkImt4T/kiKwDbENrVEnYWJmipCKP
|
||||
ribxWky7HqxDCoYMQr0zatxB3A9mx5stH+H3kbw3CZcm+ugF9ZIKDEVHb0lf28gq
|
||||
llmD120q/vs9YV3rzVL7sBGDqf6olkulvHQJKElZg2rdcHWFcngSlU2BjR04oyuH
|
||||
c/SSiLSB3YB0tdFGta5uorXyV1y7RElPeBfOfvEjsG3TAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBQX+xlgSWWbDjv0SrJ+h67xauJ80zAfBgNVHSMEGDAWgBQX+xlgSWWbDjv0
|
||||
SrJ+h67xauJ80zAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw
|
||||
ZG2MCCjscn6h/QOoJU+IDfa68OqLq0I37gMnLMde4yEhAmm//miePIq4Uz9GRJ+h
|
||||
rAmdEnspKgyQ93PjF7Xpk/JdJA4B1bIrsOl/cSwqx2sFhRt8Kt1DHGlGWXqOaHRP
|
||||
UkZ86MyRL3sXly6WkxEYxZJeQaOzMy2XmQh7grzrlTBuSI+0xf7vsRRDipxr6LVQ
|
||||
6qGWyGODLLc2JD1IXj/1HpRVT2LoGGlKMuyxACQAm4oak1vvJ9mGxgfd9AU+eo58
|
||||
O/esB2Eaf+QqMPELdFSZQfG2jvp+3WQTZK8fDKHyLr076G3UetEMy867F6fzTSZd
|
||||
9Kxq0DY7RCEpdHMCKcOL
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
messageCache: # Redis server configuration for message store cache
|
||||
persistDelayMinutes: 1
|
||||
cluster:
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 01c0bbfd6253e9532802e789dd801e9673ae4c2c
|
||||
Subproject commit 97cd9fd9bc109000228af8289999e835794ed969
|
Loading…
Reference in New Issue