Generate as well as consume auth tokens. Also user agents.
// FREEBIE
This commit is contained in:
parent
ae122ff8a2
commit
2fe9f3effa
|
@ -9,3 +9,4 @@ config/production.yml
|
|||
config/federated.yml
|
||||
config/staging.yml
|
||||
.opsmanage
|
||||
put.sh
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -75,4 +75,5 @@ public class AccountAttributes {
|
|||
public boolean getVoice() {
|
||||
return voice;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue