0
0
mirror of https://github.com/signalapp/Signal-Server.git synced 2024-09-20 03:52:16 +02:00

Add /v2/directory/auth endpoint

This commit is contained in:
Chris Eager 2021-10-08 17:30:42 -07:00 committed by Chris Eager
parent 1053a47e42
commit eb86986cf4
9 changed files with 204 additions and 79 deletions

View File

@ -83,6 +83,10 @@ directory:
replicationPassword: # CDS replication endpoint password
replicationCaCertificate: # CDS replication endpoint TLS certificate trust root
directoryV2:
client: # Configuration for interfacing with Contact Discovery Service v2 cluster
userAuthenticationTokenSharedSecret: # hex-encoded secret shared with CDS to generate auth tokens for Signal users
messageCache: # Redis server configuration for message store cache
persistDelayMinutes:

View File

@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
import org.whispersystems.textsecuregcm.configuration.DeletedAccountsDynamoDbConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryV2Configuration;
import org.whispersystems.textsecuregcm.configuration.DonationConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguration;
import org.whispersystems.textsecuregcm.configuration.DynamoDbConfiguration;
@ -125,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private DirectoryConfiguration directory;
@NotNull
@Valid
@JsonProperty
private DirectoryV2Configuration directoryV2;
@NotNull
@Valid
@JsonProperty
@ -390,6 +396,10 @@ public class WhisperServerConfiguration extends Configuration {
return directory;
}
public DirectoryV2Configuration getDirectoryV2Configuration() {
return directoryV2;
}
public SecureStorageServiceConfiguration getSecureStorageServiceConfiguration() {
return storageService;
}

View File

@ -87,6 +87,7 @@ import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.ChallengeController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
import org.whispersystems.textsecuregcm.controllers.DonationController;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysController;
@ -426,6 +427,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ExternalServiceCredentialGenerator directoryCredentialsGenerator = new ExternalServiceCredentialGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenUserIdSecret(),
true);
ExternalServiceCredentialGenerator directoryV2CredentialsGenerator = new ExternalServiceCredentialGenerator(
config.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration()
.getUserAuthenticationTokenSharedSecret(),
new byte[0], // no username derivation means no userIdKey needed
false, false);
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
new DynamicConfigurationManager<>(config.getAppConfig().getApplication(),
@ -632,6 +638,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
new ChallengeController(rateLimitChallengeManager),
new DeviceController(pendingDevicesManager, accountsManager, messagesManager, keysDynamoDb, rateLimiters, config.getMaxDevices()),
new DirectoryController(directoryCredentialsGenerator),
new DirectoryV2Controller(directoryV2CredentialsGenerator),
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
ReceiptCredentialPresentation::new, stripeExecutor, config.getDonationConfiguration(), config.getStripe()),
new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, messagesManager, unsealedSenderRateLimiter, apnFallbackManager,

View File

@ -5,93 +5,58 @@
package org.whispersystems.textsecuregcm.auth;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.Util;
import com.google.common.annotations.VisibleForTesting;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.codec.binary.Hex;
import org.whispersystems.textsecuregcm.util.Util;
public class ExternalServiceCredentialGenerator {
private final Logger logger = LoggerFactory.getLogger(ExternalServiceCredentialGenerator.class);
private final byte[] key;
private final byte[] userIdKey;
private final boolean usernameDerivation;
private final boolean prependUsername;
private final Clock clock;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
this(key, userIdKey, usernameDerivation, true);
}
public ExternalServiceCredentials generateFor(String number) {
Mac mac = getMacInstance();
String username = getUserId(number, mac, usernameDerivation);
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = prefix + ":" + output;
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername) {
this(key, userIdKey, usernameDerivation, prependUsername, Clock.systemUTC());
}
@VisibleForTesting
public ExternalServiceCredentialGenerator(byte[] key, byte[] userIdKey, boolean usernameDerivation,
boolean prependUsername, Clock clock) {
this.key = key;
this.userIdKey = userIdKey;
this.usernameDerivation = usernameDerivation;
this.prependUsername = prependUsername;
this.clock = clock;
}
public ExternalServiceCredentials generateFor(String identity) {
Mac mac = getMacInstance();
String username = getUserId(identity, mac, usernameDerivation);
long currentTimeSeconds = clock.millis() / 1000;
String prefix = username + ":" + currentTimeSeconds;
String output = Hex.encodeHexString(Util.truncate(getHmac(key, prefix.getBytes(), mac), 10));
String token = (prependUsername ? prefix : currentTimeSeconds) + ":" + output;
return new ExternalServiceCredentials(username, token);
}
public boolean isValid(String token, String number, long currentTimeMillis) {
String[] parts = token.split(":");
Mac mac = getMacInstance();
if (parts.length != 3) {
return false;
}
if (!getUserId(number, mac, usernameDerivation).equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2], mac);
}
private String getUserId(String number, Mac mac, boolean usernameDerivation) {
if (usernameDerivation) return Hex.encodeHexString(Util.truncate(getHmac(userIdKey, number.getBytes(), mac), 10));
else return number;
}
private boolean isValidTime(String timeString, long currentTimeMillis) {
try {
long tokenTime = Long.parseLong(timeString);
long ourTime = TimeUnit.MILLISECONDS.toSeconds(currentTimeMillis);
return TimeUnit.SECONDS.toHours(Math.abs(ourTime - tokenTime)) < 24;
} catch (NumberFormatException e) {
logger.warn("Number Format", e);
return false;
}
}
private boolean isValidSignature(String prefix, String suffix, Mac mac) {
try {
byte[] ourSuffix = Util.truncate(getHmac(key, prefix.getBytes(), mac), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (DecoderException e) {
logger.warn("DirectoryCredentials", e);
return false;
}
}
private Mac getMacInstance() {
try {
return Mac.getInstance("HmacSHA256");

View File

@ -0,0 +1,22 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class DirectoryV2ClientConfiguration {
@NotEmpty
@JsonProperty
private String userAuthenticationTokenSharedSecret;
public byte[] getUserAuthenticationTokenSharedSecret() throws DecoderException {
return Hex.decodeHex(userAuthenticationTokenSharedSecret.toCharArray());
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public class DirectoryV2Configuration {
@JsonProperty
@NotNull
@Valid
private DirectoryV2ClientConfiguration client;
public DirectoryV2ClientConfiguration getDirectoryV2ClientConfiguration() {
return client;
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import java.util.Base64;
import java.util.UUID;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.util.ByteUtil;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.Util;
@Path("/v2/directory")
public class DirectoryV2Controller {
private final ExternalServiceCredentialGenerator directoryServiceTokenGenerator;
public DirectoryV2Controller(ExternalServiceCredentialGenerator userTokenGenerator) {
this.directoryServiceTokenGenerator = userTokenGenerator;
}
@Timed
@GET
@Path("/auth")
@Produces(MediaType.APPLICATION_JSON)
public Response getAuthToken(@Auth AuthenticatedAccount auth) {
final UUID uuid = auth.getAccount().getUuid();
final String e164 = auth.getAccount().getNumber();
final long e164AsLong = Long.parseLong(e164, e164.indexOf('+'), e164.length() - 1, 10);
final byte[] uuidAndNumber = ByteUtil.combine(UUIDUtil.toBytes(uuid), Util.longToByteArray(e164AsLong));
final String username = Base64.getEncoder().encodeToString(uuidAndNumber);
final ExternalServiceCredentials credentials = directoryServiceTokenGenerator.generateFor(username);
return Response.ok().entity(credentials).build();
}
}

View File

@ -10,6 +10,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@ -107,16 +108,6 @@ public class Util {
return param == null || param.length() == 0;
}
public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) {
byte[] combined = new byte[one.length + two.length + three.length + four.length];
System.arraycopy(one, 0, combined, 0, one.length);
System.arraycopy(two, 0, combined, one.length, two.length);
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length);
return combined;
}
public static byte[] truncate(byte[] element, int length) {
byte[] result = new byte[length];
System.arraycopy(element, 0, result, 0, result.length);
@ -124,7 +115,6 @@ public class Util {
return result;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
@ -161,11 +151,19 @@ public class Util {
return data;
}
public static byte[] longToByteArray(long value) {
final ByteBuffer longBuffer = ByteBuffer.allocate(Long.BYTES);
longBuffer.putLong(value);
return longBuffer.array();
}
public static int toIntExact(long value) {
if ((int)value != value) {
if ((int) value != value) {
throw new ArithmeticException("integer overflow");
}
return (int)value;
return (int) value;
}
public static int currentDaysSinceEpoch() {

View File

@ -0,0 +1,49 @@
/*
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.tests.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Pair;
class DirectoryControllerV2Test {
@Test
void testAuthToken() {
final ExternalServiceCredentialGenerator credentialGenerator = new ExternalServiceCredentialGenerator(
new byte[]{0x1}, new byte[0], false, false,
Clock.fixed(Instant.ofEpochSecond(1633738643L), ZoneId.of("Etc/UTC")));
final DirectoryV2Controller controller = new DirectoryV2Controller(credentialGenerator);
final Account account = mock(Account.class);
final UUID uuid = UUID.fromString("11111111-1111-1111-1111-111111111111");
when(account.getUuid()).thenReturn(uuid);
when(account.getNumber()).thenReturn("+15055551111");
final ExternalServiceCredentials credentials = (ExternalServiceCredentials) controller.getAuthToken(
new AuthenticatedAccount(() -> new Pair<>(account, mock(Device.class)))).getEntity();
assertEquals(credentials.getUsername(), "EREREREREREREREREREREQAAAABZvPKn");
assertEquals(credentials.getPassword(), "1633738643:ff03669c64f3f938a279");
assertEquals(32, credentials.getUsername().length());
assertEquals(31, credentials.getPassword().length());
}
}