From 2fe9f3effa9163fc2fc373f51cc4c9894c955b1b Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Mon, 21 Sep 2015 14:09:03 -0700 Subject: [PATCH] Generate as well as consume auth tokens. Also user agents. // FREEBIE --- .gitignore | 1 + .../auth/AuthorizationToken.java | 57 ++---------- .../auth/AuthorizationTokenGenerator.java | 90 +++++++++++++++++++ .../controllers/AccountController.java | 60 +++++++++---- .../entities/AccountAttributes.java | 1 + .../federation/NonLimitedAccount.java | 2 +- .../textsecuregcm/storage/Account.java | 2 +- .../textsecuregcm/storage/Device.java | 15 +++- .../controllers/FederatedControllerTest.java | 6 +- .../controllers/MessageControllerTest.java | 8 +- .../controllers/ReceiptControllerTest.java | 6 +- 11 files changed, 166 insertions(+), 82 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationTokenGenerator.java diff --git a/.gitignore b/.gitignore index 1671169f0..0fac36d71 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ config/production.yml config/federated.yml config/staging.yml .opsmanage +put.sh diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationToken.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationToken.java index 7ca7af819..8da4b4d19 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationToken.java +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationToken.java @@ -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() {} } diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationTokenGenerator.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationTokenGenerator.java new file mode 100644 index 000000000..2128be691 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationTokenGenerator.java @@ -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; + } + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index a327590ea..6e3e215bf 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -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 authorizationKey; - private final Map 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 tokenGenerator; + private final Map 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); diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index b47aedd2c..e2e7cbb91 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -75,4 +75,5 @@ public class AccountAttributes { public boolean getVoice() { return voice; } + } diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java index 70cfea266..4c13c9f3d 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java @@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account { @Override public Optional 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")); } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index c9cee94ee..4d0dd1a01 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -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 getDevices() { diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 65e4a0ff6..0d6933948 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -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; diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java index 9c1616bbe..e6c8bf1c1 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java @@ -79,12 +79,12 @@ public class FederatedControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - 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 multiDeviceList = new HashSet() {{ - 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); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java index 95dbf7d16..7d49e3603 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -74,13 +74,13 @@ public class MessageControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - 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 multiDeviceList = new HashSet() {{ - 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); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java index bcc57690f..23660594c 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ReceiptControllerTest.java @@ -53,12 +53,12 @@ public class ReceiptControllerTest { @Before public void setup() throws Exception { Set singleDeviceList = new HashSet() {{ - 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 multiDeviceList = new HashSet() {{ - 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);