Generic credential auth endpoint for call links
This commit is contained in:
parent
48ebafa4e0
commit
e4da59c236
|
@ -433,3 +433,6 @@ registrationService:
|
|||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||
AAAAAAAAAAAAAAAAAAAA
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
callLink:
|
||||
userAuthenticationTokenSharedSecret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG= # base64-encoded secret shared with calling frontend to generate auth tokens for Signal users
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
|||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||
|
@ -219,6 +220,11 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
@JsonProperty
|
||||
private ArtServiceConfiguration artService;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
private CallLinkConfiguration callLink;
|
||||
|
||||
@Valid
|
||||
@NotNull
|
||||
@JsonProperty
|
||||
|
@ -371,6 +377,10 @@ public class WhisperServerConfiguration extends Configuration {
|
|||
return datadog;
|
||||
}
|
||||
|
||||
public CallLinkConfiguration getCallLinkConfiguration() {
|
||||
return callLink;
|
||||
}
|
||||
|
||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||
return unidentifiedDelivery;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
|||
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
|
@ -490,6 +491,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
config.getArtServiceConfiguration());
|
||||
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
||||
config.getSvr2Configuration());
|
||||
ExternalServiceCredentialsGenerator callLinkCredentialsGenerator = CallLinkController.credentialsGenerator(
|
||||
config.getCallLinkConfiguration()
|
||||
);
|
||||
|
||||
dynamicConfigurationManager.start();
|
||||
|
||||
|
@ -774,6 +778,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
|||
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket()),
|
||||
new AttachmentControllerV3(rateLimiters, config.getGcpAttachmentsConfiguration().getDomain(), config.getGcpAttachmentsConfiguration().getEmail(), config.getGcpAttachmentsConfiguration().getMaxSizeInBytes(), config.getGcpAttachmentsConfiguration().getPathPrefix(), config.getGcpAttachmentsConfiguration().getRsaSigningKey()),
|
||||
new CallLinkController(callLinkCredentialsGenerator),
|
||||
new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, clock),
|
||||
new ChallengeController(rateLimitChallengeManager),
|
||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
|
||||
|
|
|
@ -11,8 +11,10 @@ import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedTo
|
|||
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
|
@ -28,6 +30,10 @@ public class ExternalServiceCredentialsGenerator {
|
|||
|
||||
private final boolean truncateSignature;
|
||||
|
||||
private final String usernameTimestampPrefix;
|
||||
|
||||
private final Function<Instant, Instant> usernameTimestampTruncator;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
private final int truncateLength;
|
||||
|
@ -41,14 +47,22 @@ public class ExternalServiceCredentialsGenerator {
|
|||
final byte[] userDerivationKey,
|
||||
final boolean prependUsername,
|
||||
final boolean truncateSignature,
|
||||
final Clock clock,
|
||||
final int truncateLength) {
|
||||
final int truncateLength,
|
||||
final String usernameTimestampPrefix,
|
||||
final Function<Instant, Instant> usernameTimestampTruncator,
|
||||
final Clock clock) {
|
||||
this.key = requireNonNull(key);
|
||||
this.userDerivationKey = requireNonNull(userDerivationKey);
|
||||
this.prependUsername = prependUsername;
|
||||
this.truncateSignature = truncateSignature;
|
||||
this.usernameTimestampPrefix = usernameTimestampPrefix;
|
||||
this.usernameTimestampTruncator = usernameTimestampTruncator;
|
||||
this.clock = requireNonNull(clock);
|
||||
this.truncateLength = truncateLength;
|
||||
|
||||
if (hasUsernameTimestampPrefix() ^ hasUsernameTimestampTruncator()) {
|
||||
throw new RuntimeException("Configured to have only one of (usernameTimestampPrefix, usernameTimestampTruncator)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,13 +80,34 @@ public class ExternalServiceCredentialsGenerator {
|
|||
* @return an instance of {@link ExternalServiceCredentials}
|
||||
*/
|
||||
public ExternalServiceCredentials generateFor(final String identity) {
|
||||
if (usernameIsTimestamp()) {
|
||||
throw new RuntimeException("Configured to use timestamp as username");
|
||||
}
|
||||
|
||||
return generate(identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates `ExternalServiceCredentials` using a prefix concatenated with a truncated timestamp as the username, following this generator's configuration.
|
||||
* @return an instance of {@link ExternalServiceCredentials}
|
||||
*/
|
||||
public ExternalServiceCredentials generateWithTimestampAsUsername() {
|
||||
if (!usernameIsTimestamp()) {
|
||||
throw new RuntimeException("Not configured to use timestamp as username");
|
||||
}
|
||||
|
||||
final String truncatedTimestampSeconds = String.valueOf(usernameTimestampTruncator.apply(clock.instant()).getEpochSecond());
|
||||
return generate(usernameTimestampPrefix + DELIMITER + truncatedTimestampSeconds);
|
||||
}
|
||||
|
||||
private ExternalServiceCredentials generate(final String identity) {
|
||||
final String username = shouldDeriveUsername()
|
||||
? hmac256TruncatedToHexString(userDerivationKey, identity, truncateLength)
|
||||
: identity;
|
||||
|
||||
final long currentTimeSeconds = currentTimeSeconds();
|
||||
|
||||
final String dataToSign = username + DELIMITER + currentTimeSeconds;
|
||||
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
|
||||
|
||||
final String signature = truncateSignature
|
||||
? hmac256TruncatedToHexString(key, dataToSign, truncateLength)
|
||||
|
@ -84,7 +119,7 @@ public class ExternalServiceCredentialsGenerator {
|
|||
}
|
||||
|
||||
/**
|
||||
* In certain cases, identity (as it was passed to `generateFor` method)
|
||||
* In certain cases, identity (as it was passed to `generate` method)
|
||||
* is a part of the signature (`password`, in terms of `ExternalServiceCredentials`) string itself.
|
||||
* For such cases, this method returns the value of the identity string.
|
||||
* @param password `password` part of `ExternalServiceCredentials`
|
||||
|
@ -96,9 +131,15 @@ public class ExternalServiceCredentialsGenerator {
|
|||
return Optional.empty();
|
||||
}
|
||||
// checking for the case of unexpected format
|
||||
return StringUtils.countMatches(password, DELIMITER) == 2
|
||||
? Optional.of(password.substring(0, password.indexOf(DELIMITER)))
|
||||
: Optional.empty();
|
||||
if (StringUtils.countMatches(password, DELIMITER) == 2) {
|
||||
if (usernameIsTimestamp()) {
|
||||
final int indexOfSecondDelimiter = password.indexOf(DELIMITER, password.indexOf(DELIMITER) + 1);
|
||||
return Optional.of(password.substring(0, indexOfSecondDelimiter));
|
||||
} else {
|
||||
return Optional.of(password.substring(0, password.indexOf(DELIMITER)));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +156,7 @@ public class ExternalServiceCredentialsGenerator {
|
|||
|
||||
// making sure password format matches our expectations based on the generator configuration
|
||||
if (parts.length == 3 && prependUsername) {
|
||||
final String username = parts[0];
|
||||
final String username = usernameIsTimestamp() ? parts[0] + DELIMITER + parts[1] : parts[0];
|
||||
// username has to match the one from `credentials`
|
||||
if (!credentials.username().equals(username)) {
|
||||
return Optional.empty();
|
||||
|
@ -130,7 +171,7 @@ public class ExternalServiceCredentialsGenerator {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
final String signedData = credentials.username() + DELIMITER + timestampSeconds;
|
||||
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
|
||||
final String expectedSignature = truncateSignature
|
||||
? hmac256TruncatedToHexString(key, signedData, truncateLength)
|
||||
: hmac256ToHexString(key, signedData);
|
||||
|
@ -158,6 +199,18 @@ public class ExternalServiceCredentialsGenerator {
|
|||
return userDerivationKey.length > 0;
|
||||
}
|
||||
|
||||
private boolean hasUsernameTimestampPrefix() {
|
||||
return usernameTimestampPrefix != null;
|
||||
}
|
||||
|
||||
private boolean hasUsernameTimestampTruncator() {
|
||||
return usernameTimestampTruncator != null;
|
||||
}
|
||||
|
||||
private boolean usernameIsTimestamp() {
|
||||
return hasUsernameTimestampPrefix() && hasUsernameTimestampTruncator();
|
||||
}
|
||||
|
||||
private long currentTimeSeconds() {
|
||||
return clock.instant().getEpochSecond();
|
||||
}
|
||||
|
@ -174,6 +227,10 @@ public class ExternalServiceCredentialsGenerator {
|
|||
|
||||
private int truncateLength = 10;
|
||||
|
||||
private String usernameTimestampPrefix = null;
|
||||
|
||||
private Function<Instant, Instant> usernameTimestampTruncator = null;
|
||||
|
||||
private Clock clock = Clock.systemUTC();
|
||||
|
||||
|
||||
|
@ -208,9 +265,15 @@ public class ExternalServiceCredentialsGenerator {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withUsernameTimestampTruncatorAndPrefix(final Function<Instant, Instant> truncator, final String prefix) {
|
||||
this.usernameTimestampTruncator = truncator;
|
||||
this.usernameTimestampPrefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExternalServiceCredentialsGenerator build() {
|
||||
return new ExternalServiceCredentialsGenerator(
|
||||
key, userDerivationKey, prependUsername, truncateSignature, clock, truncateLength);
|
||||
key, userDerivationKey, prependUsername, truncateSignature, truncateLength, usernameTimestampPrefix, usernameTimestampTruncator, clock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.HexFormat;
|
||||
|
||||
public record CallLinkConfiguration (@ExactlySize({32}) byte[] userAuthenticationTokenSharedSecret) {
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
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 org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.time.Clock;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
|
||||
@Path("/v1/call-link")
|
||||
@io.swagger.v3.oas.annotations.tags.Tag(name = "CallLink")
|
||||
public class CallLinkController {
|
||||
@VisibleForTesting
|
||||
public static final String ANONYMOUS_CREDENTIAL_PREFIX = "anon";
|
||||
|
||||
private final ExternalServiceCredentialsGenerator callingFrontendServiceCredentialGenerator;
|
||||
|
||||
public CallLinkController(
|
||||
ExternalServiceCredentialsGenerator callingFrontendServiceCredentialGenerator
|
||||
) {
|
||||
this.callingFrontendServiceCredentialGenerator = callingFrontendServiceCredentialGenerator;
|
||||
}
|
||||
|
||||
public static ExternalServiceCredentialsGenerator credentialsGenerator(final CallLinkConfiguration cfg) {
|
||||
return ExternalServiceCredentialsGenerator
|
||||
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), ANONYMOUS_CREDENTIAL_PREFIX)
|
||||
.build();
|
||||
}
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/auth")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "Generate credentials for calling frontend",
|
||||
description = """
|
||||
These credentials enable clients to prove to calling frontend that they were a Signal user within the last day.
|
||||
For client privacy, timestamps are truncated to 1 day granularity and the token does not include or derive from an ACI.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
|
||||
public ExternalServiceCredentials getAuth(@Auth AuthenticatedAccount auth) {
|
||||
return callingFrontendServiceCredentialGenerator.generateWithTimestampAsUsername();
|
||||
}
|
||||
}
|
|
@ -8,9 +8,14 @@ package org.whispersystems.textsecuregcm.tests.auth;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
|
@ -18,6 +23,7 @@ import org.whispersystems.textsecuregcm.util.MockUtils;
|
|||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||
|
||||
class ExternalServiceCredentialsGeneratorTest {
|
||||
private static final String PREFIX = "prefix";
|
||||
|
||||
private static final String E164 = "+14152222222";
|
||||
|
||||
|
@ -27,6 +33,42 @@ class ExternalServiceCredentialsGeneratorTest {
|
|||
|
||||
private static final String TIME_SECONDS_STRING = Long.toString(TIME_SECONDS);
|
||||
|
||||
private static final String USERNAME_TIMESTAMP = PREFIX + ":" + Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
|
||||
|
||||
private static final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator standardGenerator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withClock(clock)
|
||||
.build();
|
||||
|
||||
private static final ExternalServiceCredentials standardCredentials = standardGenerator.generateFor(E164);
|
||||
|
||||
private static final ExternalServiceCredentialsGenerator usernameIsTimestampGenerator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), PREFIX)
|
||||
.withClock(clock)
|
||||
.build();
|
||||
|
||||
private static final ExternalServiceCredentials usernameIsTimestampCredentials = usernameIsTimestampGenerator.generateWithTimestampAsUsername();
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception {
|
||||
clock.setTimeMillis(TIME_MILLIS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidConstructor() {
|
||||
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withUsernameTimestampTruncatorAndPrefix(null, PREFIX)
|
||||
.build());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), null)
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenerateDerivedUsername() {
|
||||
|
@ -42,18 +84,13 @@ class ExternalServiceCredentialsGeneratorTest {
|
|||
|
||||
@Test
|
||||
void testGenerateNoDerivedUsername() {
|
||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.build();
|
||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
||||
assertEquals(credentials.username(), E164);
|
||||
assertTrue(credentials.password().startsWith(E164));
|
||||
assertEquals(credentials.password().split(":").length, 3);
|
||||
assertEquals(standardCredentials.username(), E164);
|
||||
assertTrue(standardCredentials.password().startsWith(E164));
|
||||
assertEquals(standardCredentials.password().split(":").length, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotPrependUsername() throws Exception {
|
||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.prependUsername(false)
|
||||
|
@ -65,52 +102,68 @@ class ExternalServiceCredentialsGeneratorTest {
|
|||
assertEquals(credentials.password().split(":").length, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithUsernameIsTimestamp() {
|
||||
assertEquals(USERNAME_TIMESTAMP, usernameIsTimestampCredentials.username());
|
||||
|
||||
final String[] passwordComponents = usernameIsTimestampCredentials.password().split(":");
|
||||
assertEquals(USERNAME_TIMESTAMP, passwordComponents[0] + ":" + passwordComponents[1]);
|
||||
assertEquals(hmac256TruncatedToHexString(new byte[32], USERNAME_TIMESTAMP, 10), passwordComponents[2]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateValid() throws Exception {
|
||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withClock(clock)
|
||||
.build();
|
||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
||||
assertEquals(generator.validateAndGetTimestamp(credentials).orElseThrow(), TIME_SECONDS);
|
||||
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials).orElseThrow(), TIME_SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateValidWithUsernameIsTimestamp() {
|
||||
final long expectedTimestamp = Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
|
||||
assertEquals(expectedTimestamp, usernameIsTimestampGenerator.validateAndGetTimestamp(usernameIsTimestampCredentials).orElseThrow());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateInvalid() throws Exception {
|
||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withClock(clock)
|
||||
.build();
|
||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
||||
final ExternalServiceCredentials corruptedStandardUsername = new ExternalServiceCredentials(
|
||||
standardCredentials.username(), standardCredentials.password().replace(E164, E164 + "0"));
|
||||
final ExternalServiceCredentials corruptedStandardTimestamp = new ExternalServiceCredentials(
|
||||
standardCredentials.username(), standardCredentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
|
||||
final ExternalServiceCredentials corruptedStandardPassword = new ExternalServiceCredentials(
|
||||
standardCredentials.username(), standardCredentials.password() + "0");
|
||||
|
||||
final ExternalServiceCredentials corruptedUsername = new ExternalServiceCredentials(
|
||||
credentials.username(), credentials.password().replace(E164, E164 + "0"));
|
||||
final ExternalServiceCredentials corruptedTimestamp = new ExternalServiceCredentials(
|
||||
credentials.username(), credentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
|
||||
final ExternalServiceCredentials corruptedPassword = new ExternalServiceCredentials(
|
||||
credentials.username(), credentials.password() + "0");
|
||||
final ExternalServiceCredentials corruptedUsernameTimestamp = new ExternalServiceCredentials(
|
||||
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password().replace(USERNAME_TIMESTAMP, USERNAME_TIMESTAMP
|
||||
+ "0"));
|
||||
final ExternalServiceCredentials corruptedUsernameTimestampPassword = new ExternalServiceCredentials(
|
||||
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password() + "0");
|
||||
|
||||
assertTrue(generator.validateAndGetTimestamp(corruptedUsername).isEmpty());
|
||||
assertTrue(generator.validateAndGetTimestamp(corruptedTimestamp).isEmpty());
|
||||
assertTrue(generator.validateAndGetTimestamp(corruptedPassword).isEmpty());
|
||||
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardUsername).isEmpty());
|
||||
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardTimestamp).isEmpty());
|
||||
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardPassword).isEmpty());
|
||||
|
||||
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestamp).isEmpty());
|
||||
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestampPassword).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateWithExpiration() throws Exception {
|
||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||
.builder(new byte[32])
|
||||
.withClock(clock)
|
||||
.build();
|
||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
||||
|
||||
final long elapsedSeconds = 10000;
|
||||
clock.incrementSeconds(elapsedSeconds);
|
||||
|
||||
assertEquals(generator.validateAndGetTimestamp(credentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
|
||||
assertTrue(generator.validateAndGetTimestamp(credentials, elapsedSeconds - 1).isEmpty());
|
||||
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
|
||||
assertTrue(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds - 1).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIdentityFromSignature() {
|
||||
final String identity = standardGenerator.identityFromSignature(standardCredentials.password()).orElseThrow();
|
||||
assertEquals(E164, identity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetIdentityFromSignatureIsTimestamp() {
|
||||
final String identity = usernameIsTimestampGenerator.identityFromSignature(usernameIsTimestampCredentials.password()).orElseThrow();
|
||||
assertEquals(USERNAME_TIMESTAMP, identity);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
|
||||
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.mockito.Mockito;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||
public class CallLinkControllerTest {
|
||||
private static final CallLinkConfiguration callLinkConfiguration = MockUtils.buildMock(
|
||||
CallLinkConfiguration.class,
|
||||
cfg -> Mockito.when(cfg.userAuthenticationTokenSharedSecret()).thenReturn(new byte[32])
|
||||
);
|
||||
private static final ExternalServiceCredentialsGenerator callLinkCredentialsGenerator = CallLinkController.credentialsGenerator(callLinkConfiguration);
|
||||
|
||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new CallLinkController(callLinkCredentialsGenerator))
|
||||
.build();
|
||||
|
||||
@Test
|
||||
void testGetAuth() {
|
||||
ExternalServiceCredentials credentials = resources.getJerseyTest()
|
||||
.target("/v1/call-link/auth")
|
||||
.request()
|
||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(ExternalServiceCredentials.class);
|
||||
|
||||
assertThat(credentials.username()).isNotEmpty();
|
||||
assertThat(credentials.password()).isNotEmpty();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue