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
	
	 Katherine Yen
						Katherine Yen