Support for signature token based account verification.

This commit is contained in:
Moxie Marlinspike
2014-11-13 14:25:33 -08:00
parent 8f2722263f
commit 222c7ea641
7 changed files with 284 additions and 38 deletions

View File

@@ -25,6 +25,7 @@ import org.whispersystems.textsecuregcm.configuration.MemcacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.MetricsConfiguration;
import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.S3Configuration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
@@ -95,6 +96,9 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private WebsocketConfiguration websocket = new WebsocketConfiguration();
@JsonProperty
private RedPhoneConfiguration redphone = new RedPhoneConfiguration();
public WebsocketConfiguration getWebsocketConfiguration() {
return websocket;
}
@@ -146,4 +150,8 @@ public class WhisperServerConfiguration extends Configuration {
public MetricsConfiguration getMetricsConfiguration() {
return viz;
}
public RedPhoneConfiguration getRedphoneConfiguration() {
return redphone;
}
}

View File

@@ -52,6 +52,7 @@ import org.whispersystems.textsecuregcm.providers.MemcacheHealthCheck;
import org.whispersystems.textsecuregcm.providers.MemcachedClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.providers.RedisHealthCheck;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.push.APNSender;
import org.whispersystems.textsecuregcm.push.GCMSender;
import org.whispersystems.textsecuregcm.push.PushSender;
@@ -155,14 +156,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(apnSender);
environment.lifecycle().manage(gcmSender);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager);
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(gcmSender, apnSender, websocketSender);
TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration());
Optional<NexmoSmsSender> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
SmsSender smsSender = new SmsSender(twilioSmsSender, nexmoSmsSender, config.getTwilioConfiguration().isInternational());
UrlSigner urlSigner = new UrlSigner(config.getS3Configuration());
PushSender pushSender = new PushSender(gcmSender, apnSender, websocketSender);
Optional<byte[]> authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey();
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
@@ -174,7 +176,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
deviceAuthenticator,
Device.class, "WhisperServer"));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, storedMessages, new TimeProvider(), authorizationKey));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController, keysControllerV1));

View File

@@ -0,0 +1,76 @@
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 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;
public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
this.token = token;
this.key = key;
}
public boolean isValid(String number, long currentTimeMillis) {
String[] parts = token.split(":");
if (parts.length != 3) {
return false;
}
if (!number.equals(parts[0])) {
return false;
}
if (!isValidTime(parts[1], currentTimeMillis)) {
return false;
}
return isValidSignature(parts[0] + ":" + parts[1], parts[2]);
}
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) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(key, "HmacSHA256"));
byte[] ourSuffix = Util.truncate(hmac.doFinal(prefix.getBytes()), 10);
byte[] theirSuffix = Hex.decodeHex(suffix.toCharArray());
return MessageDigest.isEqual(ourSuffix, theirSuffix);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
} catch (DecoderException e) {
logger.warn("Authorizationtoken", e);
return false;
}
}
}

View File

@@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class RedPhoneConfiguration {
@JsonProperty
private String authKey;
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
if (authKey == null || authKey.trim().length() == 0) {
return Optional.absent();
}
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
}
}

View File

@@ -27,7 +27,9 @@ import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.providers.TimeProvider;
import org.whispersystems.textsecuregcm.sms.SmsSender;
import org.whispersystems.textsecuregcm.sms.TwilioSmsSender;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -68,18 +70,24 @@ public class AccountController {
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final StoredMessages storedMessages;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
RateLimiters rateLimiters,
SmsSender smsSenderFactory,
StoredMessages storedMessages)
StoredMessages storedMessages,
TimeProvider timeProvider,
Optional<byte[]> authorizationKey)
{
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.storedMessages = storedMessages;
this.pendingAccounts = pendingAccounts;
this.accounts = accounts;
this.rateLimiters = rateLimiters;
this.smsSender = smsSenderFactory;
this.storedMessages = storedMessages;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
}
@Timed
@@ -145,30 +153,46 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build());
}
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
pendingAccounts.remove(number);
logger.debug("Stored device...");
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
try {
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
String number = header.getNumber();
String password = header.getPassword();
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(403).build());
}
AuthorizationToken token = new AuthorizationToken(verificationToken, authorizationKey.get());
if (!token.isValid(number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@PUT
@@ -219,6 +243,26 @@ public class AccountController {
encodedVerificationText)).build();
}
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
Account account = new Account();
account.setNumber(number);
account.setSupportsSms(accountAttributes.getSupportsSms());
account.addDevice(device);
accounts.create(account);
storedMessages.clear(new WebsocketAddress(number, Device.MASTER_ID));
pendingAccounts.remove(number);
logger.debug("Stored device...");
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
try {
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");

View File

@@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.providers;
public class TimeProvider {
public long getCurrentTimeMillis() {
return System.currentTimeMillis();
}
}