Generic credential auth endpoint for call links
This commit is contained in:
parent
48ebafa4e0
commit
e4da59c236
|
@ -433,3 +433,6 @@ registrationService:
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ/0123456789+abcdefghijklmnopqrstuvwxyz
|
||||||
AAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAA
|
||||||
-----END CERTIFICATE-----
|
-----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.AwsAttachmentsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BraintreeConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.CallLinkConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
|
||||||
|
@ -219,6 +220,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
private ArtServiceConfiguration artService;
|
private ArtServiceConfiguration artService;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
@JsonProperty
|
||||||
|
private CallLinkConfiguration callLink;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -371,6 +377,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return datadog;
|
return datadog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallLinkConfiguration getCallLinkConfiguration() {
|
||||||
|
return callLink;
|
||||||
|
}
|
||||||
|
|
||||||
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
|
||||||
return unidentifiedDelivery;
|
return unidentifiedDelivery;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
import org.whispersystems.textsecuregcm.controllers.ArtController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2;
|
||||||
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV3;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.CallLinkController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
import org.whispersystems.textsecuregcm.controllers.CertificateController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
|
||||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||||
|
@ -490,6 +491,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getArtServiceConfiguration());
|
config.getArtServiceConfiguration());
|
||||||
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
|
||||||
config.getSvr2Configuration());
|
config.getSvr2Configuration());
|
||||||
|
ExternalServiceCredentialsGenerator callLinkCredentialsGenerator = CallLinkController.credentialsGenerator(
|
||||||
|
config.getCallLinkConfiguration()
|
||||||
|
);
|
||||||
|
|
||||||
dynamicConfigurationManager.start();
|
dynamicConfigurationManager.start();
|
||||||
|
|
||||||
|
@ -774,6 +778,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new ArtController(rateLimiters, artCredentialsGenerator),
|
new ArtController(rateLimiters, artCredentialsGenerator),
|
||||||
new AttachmentControllerV2(rateLimiters, config.getAwsAttachmentsConfiguration().getAccessKey(), config.getAwsAttachmentsConfiguration().getAccessSecret(), config.getAwsAttachmentsConfiguration().getRegion(), config.getAwsAttachmentsConfiguration().getBucket()),
|
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 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 CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays()), zkAuthOperations, clock),
|
||||||
new ChallengeController(rateLimitChallengeManager),
|
new ChallengeController(rateLimitChallengeManager),
|
||||||
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keys, rateLimiters, config.getMaxDevices()),
|
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 static org.whispersystems.textsecuregcm.util.HmacUtils.hmacHexStringsEqual;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
|
@ -28,6 +30,10 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
|
|
||||||
private final boolean truncateSignature;
|
private final boolean truncateSignature;
|
||||||
|
|
||||||
|
private final String usernameTimestampPrefix;
|
||||||
|
|
||||||
|
private final Function<Instant, Instant> usernameTimestampTruncator;
|
||||||
|
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
private final int truncateLength;
|
private final int truncateLength;
|
||||||
|
@ -41,14 +47,22 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
final byte[] userDerivationKey,
|
final byte[] userDerivationKey,
|
||||||
final boolean prependUsername,
|
final boolean prependUsername,
|
||||||
final boolean truncateSignature,
|
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.key = requireNonNull(key);
|
||||||
this.userDerivationKey = requireNonNull(userDerivationKey);
|
this.userDerivationKey = requireNonNull(userDerivationKey);
|
||||||
this.prependUsername = prependUsername;
|
this.prependUsername = prependUsername;
|
||||||
this.truncateSignature = truncateSignature;
|
this.truncateSignature = truncateSignature;
|
||||||
|
this.usernameTimestampPrefix = usernameTimestampPrefix;
|
||||||
|
this.usernameTimestampTruncator = usernameTimestampTruncator;
|
||||||
this.clock = requireNonNull(clock);
|
this.clock = requireNonNull(clock);
|
||||||
this.truncateLength = truncateLength;
|
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}
|
* @return an instance of {@link ExternalServiceCredentials}
|
||||||
*/
|
*/
|
||||||
public ExternalServiceCredentials generateFor(final String identity) {
|
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()
|
final String username = shouldDeriveUsername()
|
||||||
? hmac256TruncatedToHexString(userDerivationKey, identity, truncateLength)
|
? hmac256TruncatedToHexString(userDerivationKey, identity, truncateLength)
|
||||||
: identity;
|
: identity;
|
||||||
|
|
||||||
final long currentTimeSeconds = currentTimeSeconds();
|
final long currentTimeSeconds = currentTimeSeconds();
|
||||||
|
|
||||||
final String dataToSign = username + DELIMITER + currentTimeSeconds;
|
final String dataToSign = usernameIsTimestamp() ? username : username + DELIMITER + currentTimeSeconds;
|
||||||
|
|
||||||
final String signature = truncateSignature
|
final String signature = truncateSignature
|
||||||
? hmac256TruncatedToHexString(key, dataToSign, truncateLength)
|
? 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.
|
* 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.
|
* For such cases, this method returns the value of the identity string.
|
||||||
* @param password `password` part of `ExternalServiceCredentials`
|
* @param password `password` part of `ExternalServiceCredentials`
|
||||||
|
@ -96,9 +131,15 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
// checking for the case of unexpected format
|
// checking for the case of unexpected format
|
||||||
return StringUtils.countMatches(password, DELIMITER) == 2
|
if (StringUtils.countMatches(password, DELIMITER) == 2) {
|
||||||
? Optional.of(password.substring(0, password.indexOf(DELIMITER)))
|
if (usernameIsTimestamp()) {
|
||||||
: Optional.empty();
|
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
|
// making sure password format matches our expectations based on the generator configuration
|
||||||
if (parts.length == 3 && prependUsername) {
|
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`
|
// username has to match the one from `credentials`
|
||||||
if (!credentials.username().equals(username)) {
|
if (!credentials.username().equals(username)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
@ -130,7 +171,7 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
final String signedData = credentials.username() + DELIMITER + timestampSeconds;
|
final String signedData = usernameIsTimestamp() ? credentials.username() : credentials.username() + DELIMITER + timestampSeconds;
|
||||||
final String expectedSignature = truncateSignature
|
final String expectedSignature = truncateSignature
|
||||||
? hmac256TruncatedToHexString(key, signedData, truncateLength)
|
? hmac256TruncatedToHexString(key, signedData, truncateLength)
|
||||||
: hmac256ToHexString(key, signedData);
|
: hmac256ToHexString(key, signedData);
|
||||||
|
@ -158,6 +199,18 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
return userDerivationKey.length > 0;
|
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() {
|
private long currentTimeSeconds() {
|
||||||
return clock.instant().getEpochSecond();
|
return clock.instant().getEpochSecond();
|
||||||
}
|
}
|
||||||
|
@ -174,6 +227,10 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
|
|
||||||
private int truncateLength = 10;
|
private int truncateLength = 10;
|
||||||
|
|
||||||
|
private String usernameTimestampPrefix = null;
|
||||||
|
|
||||||
|
private Function<Instant, Instant> usernameTimestampTruncator = null;
|
||||||
|
|
||||||
private Clock clock = Clock.systemUTC();
|
private Clock clock = Clock.systemUTC();
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,9 +265,15 @@ public class ExternalServiceCredentialsGenerator {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder withUsernameTimestampTruncatorAndPrefix(final Function<Instant, Instant> truncator, final String prefix) {
|
||||||
|
this.usernameTimestampTruncator = truncator;
|
||||||
|
this.usernameTimestampPrefix = prefix;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ExternalServiceCredentialsGenerator build() {
|
public ExternalServiceCredentialsGenerator build() {
|
||||||
return new ExternalServiceCredentialsGenerator(
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
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.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 java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
@ -18,6 +23,7 @@ import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.MutableClock;
|
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||||
|
|
||||||
class ExternalServiceCredentialsGeneratorTest {
|
class ExternalServiceCredentialsGeneratorTest {
|
||||||
|
private static final String PREFIX = "prefix";
|
||||||
|
|
||||||
private static final String E164 = "+14152222222";
|
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 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
|
@Test
|
||||||
void testGenerateDerivedUsername() {
|
void testGenerateDerivedUsername() {
|
||||||
|
@ -42,18 +84,13 @@ class ExternalServiceCredentialsGeneratorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGenerateNoDerivedUsername() {
|
void testGenerateNoDerivedUsername() {
|
||||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
assertEquals(standardCredentials.username(), E164);
|
||||||
.builder(new byte[32])
|
assertTrue(standardCredentials.password().startsWith(E164));
|
||||||
.build();
|
assertEquals(standardCredentials.password().split(":").length, 3);
|
||||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
|
||||||
assertEquals(credentials.username(), E164);
|
|
||||||
assertTrue(credentials.password().startsWith(E164));
|
|
||||||
assertEquals(credentials.password().split(":").length, 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testNotPrependUsername() throws Exception {
|
public void testNotPrependUsername() throws Exception {
|
||||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
|
||||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
||||||
.builder(new byte[32])
|
.builder(new byte[32])
|
||||||
.prependUsername(false)
|
.prependUsername(false)
|
||||||
|
@ -65,52 +102,68 @@ class ExternalServiceCredentialsGeneratorTest {
|
||||||
assertEquals(credentials.password().split(":").length, 2);
|
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
|
@Test
|
||||||
public void testValidateValid() throws Exception {
|
public void testValidateValid() throws Exception {
|
||||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials).orElseThrow(), TIME_SECONDS);
|
||||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
}
|
||||||
.builder(new byte[32])
|
|
||||||
.withClock(clock)
|
@Test
|
||||||
.build();
|
public void testValidateValidWithUsernameIsTimestamp() {
|
||||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
final long expectedTimestamp = Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
|
||||||
assertEquals(generator.validateAndGetTimestamp(credentials).orElseThrow(), TIME_SECONDS);
|
assertEquals(expectedTimestamp, usernameIsTimestampGenerator.validateAndGetTimestamp(usernameIsTimestampCredentials).orElseThrow());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateInvalid() throws Exception {
|
public void testValidateInvalid() throws Exception {
|
||||||
final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
|
final ExternalServiceCredentials corruptedStandardUsername = new ExternalServiceCredentials(
|
||||||
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
|
standardCredentials.username(), standardCredentials.password().replace(E164, E164 + "0"));
|
||||||
.builder(new byte[32])
|
final ExternalServiceCredentials corruptedStandardTimestamp = new ExternalServiceCredentials(
|
||||||
.withClock(clock)
|
standardCredentials.username(), standardCredentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
|
||||||
.build();
|
final ExternalServiceCredentials corruptedStandardPassword = new ExternalServiceCredentials(
|
||||||
final ExternalServiceCredentials credentials = generator.generateFor(E164);
|
standardCredentials.username(), standardCredentials.password() + "0");
|
||||||
|
|
||||||
final ExternalServiceCredentials corruptedUsername = new ExternalServiceCredentials(
|
final ExternalServiceCredentials corruptedUsernameTimestamp = new ExternalServiceCredentials(
|
||||||
credentials.username(), credentials.password().replace(E164, E164 + "0"));
|
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password().replace(USERNAME_TIMESTAMP, USERNAME_TIMESTAMP
|
||||||
final ExternalServiceCredentials corruptedTimestamp = new ExternalServiceCredentials(
|
+ "0"));
|
||||||
credentials.username(), credentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
|
final ExternalServiceCredentials corruptedUsernameTimestampPassword = new ExternalServiceCredentials(
|
||||||
final ExternalServiceCredentials corruptedPassword = new ExternalServiceCredentials(
|
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password() + "0");
|
||||||
credentials.username(), credentials.password() + "0");
|
|
||||||
|
|
||||||
assertTrue(generator.validateAndGetTimestamp(corruptedUsername).isEmpty());
|
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardUsername).isEmpty());
|
||||||
assertTrue(generator.validateAndGetTimestamp(corruptedTimestamp).isEmpty());
|
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardTimestamp).isEmpty());
|
||||||
assertTrue(generator.validateAndGetTimestamp(corruptedPassword).isEmpty());
|
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardPassword).isEmpty());
|
||||||
|
|
||||||
|
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestamp).isEmpty());
|
||||||
|
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestampPassword).isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateWithExpiration() throws Exception {
|
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;
|
final long elapsedSeconds = 10000;
|
||||||
clock.incrementSeconds(elapsedSeconds);
|
clock.incrementSeconds(elapsedSeconds);
|
||||||
|
|
||||||
assertEquals(generator.validateAndGetTimestamp(credentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
|
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
|
||||||
assertTrue(generator.validateAndGetTimestamp(credentials, elapsedSeconds - 1).isEmpty());
|
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
|
@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