Generate as well as consume auth tokens. Also user agents.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-09-21 14:09:03 -07:00
parent ae122ff8a2
commit 2fe9f3effa
11 changed files with 166 additions and 82 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ config/production.yml
config/federated.yml
config/staging.yml
.opsmanage
put.sh

View File

@ -1,6 +1,7 @@
package org.whispersystems.textsecuregcm.auth;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
@ -16,61 +17,13 @@ import java.util.concurrent.TimeUnit;
public class AuthorizationToken {
private final Logger logger = LoggerFactory.getLogger(AuthorizationToken.class);
@JsonProperty
private String token;
private final String token;
private final byte[] key;
public AuthorizationToken(String token, byte[] key) {
public AuthorizationToken(String token) {
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;
}
}
public AuthorizationToken() {}
}

View File

@ -0,0 +1,90 @@
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 AuthorizationTokenGenerator {
private final Logger logger = LoggerFactory.getLogger(AuthorizationTokenGenerator.class);
private final byte[] key;
public AuthorizationTokenGenerator(byte[] key) {
this.key = key;
}
public AuthorizationToken generateFor(String number) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
long currentTimeSeconds = System.currentTimeMillis() / 1000;
String prefix = number + ":" + currentTimeSeconds;
mac.init(new SecretKeySpec(key, "HmacSHA256"));
String output = Hex.encodeHexString(Util.truncate(mac.doFinal(prefix.getBytes()), 10));
String token = prefix + ":" + output;
return new AuthorizationToken(token);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public boolean isValid(String token, 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

@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
import org.whispersystems.textsecuregcm.auth.AuthorizationToken;
import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator;
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
@ -65,14 +66,14 @@ public class AccountController {
private final Logger logger = LoggerFactory.getLogger(AccountController.class);
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<byte[]> authorizationKey;
private final Map<String, Integer> testDevices;
private final PendingAccountsManager pendingAccounts;
private final AccountsManager accounts;
private final RateLimiters rateLimiters;
private final SmsSender smsSender;
private final MessagesManager messagesManager;
private final TimeProvider timeProvider;
private final Optional<AuthorizationTokenGenerator> tokenGenerator;
private final Map<String, Integer> testDevices;
public AccountController(PendingAccountsManager pendingAccounts,
AccountsManager accounts,
@ -89,8 +90,13 @@ public class AccountController {
this.smsSender = smsSenderFactory;
this.messagesManager = messagesManager;
this.timeProvider = timeProvider;
this.authorizationKey = authorizationKey;
this.testDevices = testDevices;
if (authorizationKey.isPresent()) {
tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get()));
} else {
tokenGenerator = Optional.absent();
}
}
@Timed
@ -136,6 +142,7 @@ public class AccountController {
@Path("/code/{verification_code}")
public void verifyAccount(@PathParam("verification_code") String verificationCode,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
@ -158,7 +165,7 @@ public class AccountController {
throw new WebApplicationException(Response.status(417).build());
}
createAccount(number, password, accountAttributes);
createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad Authorization Header", e);
throw new WebApplicationException(Response.status(401).build());
@ -171,6 +178,7 @@ public class AccountController {
@Path("/token/{verification_token}")
public void verifyToken(@PathParam("verification_token") String verificationToken,
@HeaderParam("Authorization") String authorizationHeader,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes accountAttributes)
throws RateLimitExceededException
{
@ -181,24 +189,37 @@ public class AccountController {
rateLimiters.getVerifyLimiter().validate(number);
if (!authorizationKey.isPresent()) {
if (!tokenGenerator.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())) {
if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) {
throw new WebApplicationException(Response.status(403).build());
}
createAccount(number, password, accountAttributes);
createAccount(number, password, userAgent, accountAttributes);
} catch (InvalidAuthorizationHeaderException e) {
logger.info("Bad authorization header", e);
throw new WebApplicationException(Response.status(401).build());
}
}
@Timed
@GET
@Path("/token/")
@Produces(MediaType.APPLICATION_JSON)
public AuthorizationToken verifyToken(@Auth Account account)
throws RateLimitExceededException
{
if (!tokenGenerator.isPresent()) {
logger.debug("Attempt to authorize with key but not configured...");
throw new WebApplicationException(Response.status(404).build());
}
return tokenGenerator.get().generateFor(account.getNumber());
}
@Timed
@PUT
@Path("/gcm/")
@ -251,7 +272,10 @@ public class AccountController {
@Timed
@PUT
@Path("/attributes/")
public void setAccountAttributes(@Auth Account account, @Valid AccountAttributes attributes) {
public void setAccountAttributes(@Auth Account account,
@HeaderParam("X-Signal-Agent") String userAgent,
@Valid AccountAttributes attributes)
{
Device device = account.getAuthenticatedDevice().get();
device.setFetchesMessages(attributes.getFetchesMessages());
@ -260,6 +284,7 @@ public class AccountController {
device.setVoiceSupported(attributes.getVoice());
device.setRegistrationId(attributes.getRegistrationId());
device.setSignalingKey(attributes.getSignalingKey());
device.setUserAgent(userAgent);
accounts.update(account);
}
@ -273,7 +298,7 @@ public class AccountController {
encodedVerificationText)).build();
}
private void createAccount(String number, String password, AccountAttributes accountAttributes) {
private void createAccount(String number, String password, String userAgent, AccountAttributes accountAttributes) {
Device device = new Device();
device.setId(Device.MASTER_ID);
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
@ -284,6 +309,7 @@ public class AccountController {
device.setVoiceSupported(accountAttributes.getVoice());
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(userAgent);
Account account = new Account();
account.setNumber(number);

View File

@ -75,4 +75,5 @@ public class AccountAttributes {
public boolean getVoice() {
return voice;
}
}

View File

@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "NA"));
}
}

View File

@ -71,7 +71,7 @@ public class Account {
}
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false));
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false, "NA"));
}
public Set<Device> getDevices() {

View File

@ -73,13 +73,17 @@ public class Device {
@JsonProperty
private boolean voice;
@JsonProperty
private String userAgent;
public Device() {}
public Device(long id, String name, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, boolean voice)
long lastSeen, long created, boolean voice,
String userAgent)
{
this.id = id;
this.name = name;
@ -95,6 +99,7 @@ public class Device {
this.lastSeen = lastSeen;
this.created = created;
this.voice = voice;
this.userAgent = userAgent;
}
public String getApnId() {
@ -225,6 +230,14 @@ public class Device {
return pushTimestamp;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
public String getUserAgent() {
return this.userAgent;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Device)) return false;

View File

@ -79,12 +79,12 @@ public class FederatedControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);

View File

@ -74,13 +74,13 @@ public class MessageControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false, "Test"));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);

View File

@ -53,12 +53,12 @@ public class ReceiptControllerTest {
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, null, System.currentTimeMillis(), System.currentTimeMillis(), false, "Test"));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);