From 4fdbe9b9ff615b98ea52cac615a584a6d92d5c94 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 6 Jun 2019 17:31:07 -0700 Subject: [PATCH] Support for push preauth --- .../textsecuregcm/WhisperServerService.java | 2 +- .../auth/StoredVerificationCode.java | 19 ++- .../controllers/AccountController.java | 99 +++++++++++++--- .../controllers/DeviceController.java | 3 +- .../textsecuregcm/push/APNSender.java | 2 + .../push/ApnFallbackManager.java | 2 +- .../textsecuregcm/push/ApnMessage.java | 38 ++++-- .../textsecuregcm/push/GCMSender.java | 14 ++- .../textsecuregcm/push/GcmMessage.java | 37 ++++-- .../textsecuregcm/push/PushSender.java | 7 +- .../storage/PendingAccounts.java | 23 ++-- .../storage/PendingAccountsManager.java | 2 +- .../textsecuregcm/storage/PendingDevices.java | 2 +- .../StoredVerificationCodeRowMapper.java | 3 +- .../textsecuregcm/util/Hex.java | 10 +- .../textsecuregcm/util/Util.java | 4 + service/src/main/resources/accountsdb.xml | 11 ++ .../controllers/AccountControllerTest.java | 112 +++++++++++++++++- .../controllers/DeviceControllerTest.java | 4 +- .../tests/push/APNSenderTest.java | 24 ++-- .../tests/push/GCMSenderTest.java | 6 +- .../tests/storage/PendingAccountsTest.java | 70 +++++++++-- 22 files changed, 391 insertions(+), 103 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index ba170e0ea..b8a0cb670 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -247,7 +247,7 @@ public class WhisperServerService extends Application(ImmutableSet.of(Account.class, DisabledPermittedAccount.class))); - environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient)); + environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, abusiveHostRules, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices(), recaptchaClient, gcmSender, apnSender)); environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices())); environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator)); environment.jersey().register(new ProvisioningController(rateLimiters, pushSender)); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java index d2700d84d..761ea6fad 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java @@ -2,6 +2,8 @@ package org.whispersystems.textsecuregcm.auth; import com.fasterxml.jackson.annotation.JsonProperty; +import org.whispersystems.textsecuregcm.util.Util; + import java.security.MessageDigest; import java.util.concurrent.TimeUnit; @@ -13,11 +15,15 @@ public class StoredVerificationCode { @JsonProperty private long timestamp; + @JsonProperty + private String pushCode; + public StoredVerificationCode() {} - public StoredVerificationCode(String code, long timestamp) { + public StoredVerificationCode(String code, long timestamp, String pushCode) { this.code = code; this.timestamp = timestamp; + this.pushCode = pushCode; } public String getCode() { @@ -28,8 +34,16 @@ public class StoredVerificationCode { return timestamp; } + public String getPushCode() { + return pushCode; + } + public boolean isValid(String theirCodeString) { - if (timestamp + TimeUnit.MINUTES.toMillis(30) < System.currentTimeMillis()) { + if (timestamp + TimeUnit.MINUTES.toMillis(10) < System.currentTimeMillis()) { + return false; + } + + if (Util.isEmpty(code) || Util.isEmpty(theirCodeString)) { return false; } @@ -38,4 +52,5 @@ public class StoredVerificationCode { return MessageDigest.isEqual(ourCode, theirCode); } + } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 687653145..ab3de1537 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -37,6 +37,10 @@ import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; import org.whispersystems.textsecuregcm.entities.RegistrationLock; import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.push.APNSender; +import org.whispersystems.textsecuregcm.push.ApnMessage; +import org.whispersystems.textsecuregcm.push.GCMSender; +import org.whispersystems.textsecuregcm.push.GcmMessage; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; @@ -48,6 +52,7 @@ import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.MessagesManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.util.Constants; +import org.whispersystems.textsecuregcm.util.Hex; import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.VerificationCode; @@ -90,16 +95,18 @@ public class AccountController { private final Meter captchaFailureMeter = metricRegistry.meter(name(AccountController.class, "captcha_failure" )); - private final PendingAccountsManager pendingAccounts; - private final AccountsManager accounts; - private final AbusiveHostRules abusiveHostRules; - private final RateLimiters rateLimiters; - private final SmsSender smsSender; - private final DirectoryQueue directoryQueue; - private final MessagesManager messagesManager; - private final TurnTokenGenerator turnTokenGenerator; - private final Map testDevices; - private final RecaptchaClient recaptchaClient; + private final PendingAccountsManager pendingAccounts; + private final AccountsManager accounts; + private final AbusiveHostRules abusiveHostRules; + private final RateLimiters rateLimiters; + private final SmsSender smsSender; + private final DirectoryQueue directoryQueue; + private final MessagesManager messagesManager; + private final TurnTokenGenerator turnTokenGenerator; + private final Map testDevices; + private final RecaptchaClient recaptchaClient; + private final GCMSender gcmSender; + private final APNSender apnSender; public AccountController(PendingAccountsManager pendingAccounts, AccountsManager accounts, @@ -110,7 +117,9 @@ public class AccountController { MessagesManager messagesManager, TurnTokenGenerator turnTokenGenerator, Map testDevices, - RecaptchaClient recaptchaClient) + RecaptchaClient recaptchaClient, + GCMSender gcmSender, + APNSender apnSender) { this.pendingAccounts = pendingAccounts; this.accounts = accounts; @@ -122,6 +131,41 @@ public class AccountController { this.testDevices = testDevices; this.turnTokenGenerator = turnTokenGenerator; this.recaptchaClient = recaptchaClient; + this.gcmSender = gcmSender; + this.apnSender = apnSender; + } + + @Timed + @GET + @Path("/{type}/preauth/{token}/{number}") + public Response getPreAuth(@PathParam("type") String pushType, + @PathParam("token") String pushToken, + @PathParam("number") String number) + { + if (!"apn".equals(pushType) && !"fcm".equals(pushType)) { + return Response.status(400).build(); + } + + if (!Util.isValidNumber(number)) { + return Response.status(400).build(); + } + + String pushChallenge = generatePushChallenge(); + StoredVerificationCode storedVerificationCode = new StoredVerificationCode(null, + System.currentTimeMillis(), + pushChallenge); + + pendingAccounts.store(number, storedVerificationCode); + + if ("fcm".equals(pushType)) { + gcmSender.sendMessage(new GcmMessage(pushToken, number, 0, GcmMessage.Type.CHALLENGE, Optional.of(storedVerificationCode.getPushCode()))); + } else if ("apn".equals(pushType)) { + apnSender.sendMessage(new ApnMessage(pushToken, number, 0, true, Optional.of(storedVerificationCode.getPushCode()))); + } else { + throw new AssertionError(); + } + + return Response.ok().build(); } @Timed @@ -132,7 +176,8 @@ public class AccountController { @HeaderParam("X-Forwarded-For") String forwardedFor, @HeaderParam("Accept-Language") Optional locale, @QueryParam("client") Optional client, - @QueryParam("captcha") Optional captcha) + @QueryParam("captcha") Optional captcha, + @QueryParam("challenge") Optional pushChallenge) throws RateLimitExceededException { if (!Util.isValidNumber(number)) { @@ -145,7 +190,8 @@ public class AccountController { .reduce((a, b) -> b) .orElseThrow(); - CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha); + Optional storedChallenge = pendingAccounts.getCodeForNumber(number); + CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, requester, captcha, storedChallenge, pushChallenge); if (requirement.isCaptchaRequired()) { if (requirement.isAutoBlock() && shouldAutoBlock(requester)) { @@ -170,7 +216,8 @@ public class AccountController { VerificationCode verificationCode = generateVerificationCode(number); StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), - System.currentTimeMillis()); + System.currentTimeMillis(), + storedChallenge.map(StoredVerificationCode::getPushCode).orElse(null)); pendingAccounts.store(number, storedVerificationCode); @@ -397,7 +444,10 @@ public class AccountController { } private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor, - String requester, Optional captchaToken) + String requester, + Optional captchaToken, + Optional storedVerificationCode, + Optional pushChallenge) { if (captchaToken.isPresent()) { @@ -412,6 +462,14 @@ public class AccountController { } } + if (pushChallenge.isPresent()) { + Optional storedPushChallenge = storedVerificationCode.map(StoredVerificationCode::getPushCode); + + if (!pushChallenge.get().equals(storedPushChallenge.orElse(null))) { + return new CaptchaRequirement(true, false); + } + } + List abuseRules = abusiveHostRules.getAbusiveHostRulesFor(requester); for (AbusiveHostRule abuseRule : abuseRules) { @@ -493,7 +551,8 @@ public class AccountController { pendingAccounts.remove(number); } - @VisibleForTesting protected VerificationCode generateVerificationCode(String number) { + @VisibleForTesting protected + VerificationCode generateVerificationCode(String number) { if (testDevices.containsKey(number)) { return new VerificationCode(testDevices.get(number)); } @@ -503,6 +562,14 @@ public class AccountController { return new VerificationCode(randomInt); } + private String generatePushChallenge() { + SecureRandom random = new SecureRandom(); + byte[] challenge = new byte[16]; + random.nextBytes(challenge); + + return Hex.toStringCondensed(challenge); + } + private static class CaptchaRequirement { private final boolean captchaRequired; private final boolean autoBlock; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index 5d08eb336..9cdbc75cb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -144,7 +144,8 @@ public class DeviceController { VerificationCode verificationCode = generateVerificationCode(); StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), - System.currentTimeMillis()); + System.currentTimeMillis(), + null); pendingDevices.store(account.getNumber(), storedVerificationCode); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java index d1a9189c1..e71bd04ad 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java @@ -94,6 +94,8 @@ public class APNSender implements Managed { Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(@Nullable ApnResult result) { + if (message.getChallengeData().isPresent()) return; + if (result == null) { logger.warn("*** RECEIVED NULL APN RESULT ***"); } else if (result.getStatus() == ApnResult.Status.NO_SUCH_USER) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnFallbackManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnFallbackManager.java index 7f8642e4b..e863f6377 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnFallbackManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnFallbackManager.java @@ -157,7 +157,7 @@ public class ApnFallbackManager implements Managed, Runnable { continue; } - apnSender.sendMessage(new ApnMessage(apnId, separated.get().first(), separated.get().second(), true)); + apnSender.sendMessage(new ApnMessage(apnId, separated.get().first(), separated.get().second(), true, Optional.empty())); retry.mark(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnMessage.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnMessage.java index c87b372e4..0b7e9f0c5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnMessage.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnMessage.java @@ -1,20 +1,28 @@ package org.whispersystems.textsecuregcm.push; +import com.google.common.annotations.VisibleForTesting; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class ApnMessage { - public static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}"; - public static final long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L; + public static final String APN_NOTIFICATION_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}}"; + public static final String APN_CHALLENGE_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"alert\":{\"loc-key\":\"APN_Message\"}}, \"challenge\" : \"%s\"}"; + public static final long MAX_EXPIRATION = Integer.MAX_VALUE * 1000L; - private final String apnId; - private final String number; - private final long deviceId; - private final boolean isVoip; + private final String apnId; + private final String number; + private final long deviceId; + private final boolean isVoip; + private final Optional challengeData; - public ApnMessage(String apnId, String number, long deviceId, boolean isVoip) { - this.apnId = apnId; - this.number = number; - this.deviceId = deviceId; - this.isVoip = isVoip; + public ApnMessage(String apnId, String number, long deviceId, boolean isVoip, Optional challengeData) { + this.apnId = apnId; + this.number = number; + this.deviceId = deviceId; + this.isVoip = isVoip; + this.challengeData = challengeData; } public boolean isVoip() { @@ -26,7 +34,13 @@ public class ApnMessage { } public String getMessage() { - return APN_PAYLOAD; + if (!challengeData.isPresent()) return APN_NOTIFICATION_PAYLOAD; + else return String.format(APN_CHALLENGE_PAYLOAD, challengeData.get()); + } + + @VisibleForTesting + public Optional getChallengeData() { + return challengeData; } public long getExpirationTime() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/GCMSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/GCMSender.java index 744f1d006..c409f77ec 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/GCMSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/GCMSender.java @@ -66,14 +66,22 @@ public class GCMSender implements Managed { .withDestination(message.getGcmId()) .withPriority("high"); - String key = message.isReceipt() ? "receipt" : "notification"; - Message request = builder.withDataPart(key, "").build(); + String key; + + switch (message.getType()) { + case RECEIPT: key = "receipt"; break; + case NOTIFICATION: key = "notification"; break; + case CHALLENGE: key = "challenge"; break; + default: throw new AssertionError(); + } + + Message request = builder.withDataPart(key, message.getData().orElse("")).build(); CompletableFuture future = signalSender.send(request); markOutboundMeter(key); future.handle((result, throwable) -> { - if (result != null) { + if (result != null && message.getType() != GcmMessage.Type.CHALLENGE) { if (result.isUnregistered() || result.isInvalidRegistrationId()) { executor.submit(() -> handleBadRegistration(message)); } else if (result.hasCanonicalRegistrationId()) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/GcmMessage.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/GcmMessage.java index 76cd97fce..4658a30dc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/GcmMessage.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/GcmMessage.java @@ -1,17 +1,27 @@ package org.whispersystems.textsecuregcm.push; + +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") public class GcmMessage { - private final String gcmId; - private final String number; - private final int deviceId; - private final boolean receipt; + public enum Type { + RECEIPT, NOTIFICATION, CHALLENGE + } - public GcmMessage(String gcmId, String number, int deviceId, boolean receipt) { - this.gcmId = gcmId; - this.number = number; - this.deviceId = deviceId; - this.receipt = receipt; + private final String gcmId; + private final String number; + private final int deviceId; + private final Type type; + private final Optional data; + + public GcmMessage(String gcmId, String number, int deviceId, Type type, Optional data) { + this.gcmId = gcmId; + this.number = number; + this.deviceId = deviceId; + this.type = type; + this.data = data; } public String getGcmId() { @@ -22,11 +32,16 @@ public class GcmMessage { return number; } - public boolean isReceipt() { - return receipt; + public Type getType() { + return type; } public int getDeviceId() { return deviceId; } + + public Optional getData() { + return data; + } + } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java index 1b496c0d2..2114b81d7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java @@ -28,6 +28,7 @@ import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Util; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static com.codahale.metrics.MetricRegistry.name; @@ -105,7 +106,7 @@ public class PushSender implements Managed { private void sendGcmNotification(Account account, Device device) { GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(), - (int)device.getId(), false); + (int)device.getId(), GcmMessage.Type.NOTIFICATION, Optional.empty()); gcmSender.sendMessage(gcmMessage); } @@ -126,10 +127,10 @@ public class PushSender implements Managed { } if (!Util.isEmpty(device.getVoipApnId())) { - apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), device.getId(), true); + apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), device.getId(), true, Optional.empty()); RedisOperation.unchecked(() -> apnFallbackManager.schedule(account, device)); } else { - apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), device.getId(), false); + apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), device.getId(), false, Optional.empty()); } apnSender.sendMessage(apnMessage); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java index 7950350f7..f676fad25 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java @@ -19,17 +19,10 @@ package org.whispersystems.textsecuregcm.storage; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.SharedMetricRegistries; import com.codahale.metrics.Timer; -import org.jdbi.v3.core.mapper.RowMapper; -import org.jdbi.v3.core.statement.PreparedBatch; -import org.jdbi.v3.core.statement.StatementContext; -import org.jdbi.v3.core.transaction.TransactionIsolationLevel; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.storage.mappers.StoredVerificationCodeRowMapper; import org.whispersystems.textsecuregcm.util.Constants; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.List; import java.util.Optional; import static com.codahale.metrics.MetricRegistry.name; @@ -49,17 +42,17 @@ public class PendingAccounts { this.database.getDatabase().registerRowMapper(new StoredVerificationCodeRowMapper()); } - public void insert(String number, String verificationCode, long timestamp) { - database.use(jdbi -> jdbi.useTransaction(TransactionIsolationLevel.SERIALIZABLE, handle -> { + public void insert(String number, String verificationCode, long timestamp, String pushCode) { + database.use(jdbi -> jdbi.useHandle(handle -> { try (Timer.Context ignored = insertTimer.time()) { - handle.createUpdate("DELETE FROM pending_accounts WHERE number = :number") - .bind("number", number) - .execute(); - - handle.createUpdate("INSERT INTO pending_accounts (number, verification_code, timestamp) VALUES (:number, :verification_code, :timestamp)") + handle.createUpdate("INSERT INTO pending_accounts (number, verification_code, timestamp, push_code) " + + "VALUES (:number, :verification_code, :timestamp, :push_code) " + + "ON CONFLICT(number) DO UPDATE " + + "SET verification_code = EXCLUDED.verification_code, timestamp = EXCLUDED.timestamp, push_code = EXCLUDED.push_code") .bind("verification_code", verificationCode) .bind("timestamp", timestamp) .bind("number", number) + .bind("push_code", pushCode) .execute(); } })); @@ -68,7 +61,7 @@ public class PendingAccounts { public Optional getCodeForNumber(String number) { return database.with(jdbi ->jdbi.withHandle(handle -> { try (Timer.Context ignored = getCodeForNumberTimer.time()) { - return handle.createQuery("SELECT verification_code, timestamp FROM pending_accounts WHERE number = :number") + return handle.createQuery("SELECT verification_code, timestamp, push_code FROM pending_accounts WHERE number = :number") .bind("number", number) .mapTo(StoredVerificationCode.class) .findFirst(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java index 17dd31ea8..8b74cb5e1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java @@ -48,7 +48,7 @@ public class PendingAccountsManager { public void store(String number, StoredVerificationCode code) { memcacheSet(number, code); - pendingAccounts.insert(number, code.getCode(), code.getTimestamp()); + pendingAccounts.insert(number, code.getCode(), code.getTimestamp(), code.getPushCode()); } public void remove(String number) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java index 9cce4d7ac..4cf395497 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java @@ -57,7 +57,7 @@ public class PendingDevices { public Optional getCodeForNumber(String number) { return database.with(jdbi -> jdbi.withHandle(handle -> { try (Timer.Context timer = getCodeForNumberTimer.time()) { - return handle.createQuery("SELECT verification_code, timestamp FROM pending_devices WHERE number = :number") + return handle.createQuery("SELECT verification_code, timestamp, NULL as push_code FROM pending_devices WHERE number = :number") .bind("number", number) .mapTo(StoredVerificationCode.class) .findFirst(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/mappers/StoredVerificationCodeRowMapper.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/mappers/StoredVerificationCodeRowMapper.java index a7bad5304..65b5a383d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/mappers/StoredVerificationCodeRowMapper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/mappers/StoredVerificationCodeRowMapper.java @@ -12,6 +12,7 @@ public class StoredVerificationCodeRowMapper implements RowMapper + + + + + + + + + + + diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 8347a117b..d1baa6f37 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -1,10 +1,12 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.collect.ImmutableSet; +import net.sourceforge.argparse4j.inf.Argument; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; @@ -19,6 +21,10 @@ import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.providers.TimeProvider; +import org.whispersystems.textsecuregcm.push.APNSender; +import org.whispersystems.textsecuregcm.push.ApnMessage; +import org.whispersystems.textsecuregcm.push.GCMSender; +import org.whispersystems.textsecuregcm.push.GcmMessage; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; @@ -53,6 +59,7 @@ public class AccountControllerTest { private static final String SENDER_PIN = "+14153333333"; private static final String SENDER_OVER_PIN = "+14154444444"; private static final String SENDER_OVER_PREFIX = "+14156666666"; + private static final String SENDER_PREAUTH = "+14157777777"; private static final String ABUSIVE_HOST = "192.168.1.1"; private static final String RESTRICTED_HOST = "192.168.1.2"; @@ -80,6 +87,8 @@ public class AccountControllerTest { private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private Account senderPinAccount = mock(Account.class); private RecaptchaClient recaptchaClient = mock(RecaptchaClient.class); + private GCMSender gcmSender = mock(GCMSender.class); + private APNSender apnSender = mock(APNSender.class); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() @@ -97,7 +106,9 @@ public class AccountControllerTest { storedMessages, turnTokenGenerator, new HashMap<>(), - recaptchaClient)) + recaptchaClient, + gcmSender, + apnSender)) .build(); @@ -116,15 +127,17 @@ public class AccountControllerTest { when(senderPinAccount.getPin()).thenReturn(Optional.of("31337")); when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis()))); - when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31)))); - when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis()))); - when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis()))); + when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis(), null))); + when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31), null))); + when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis(), null))); + when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis(), null))); + when(pendingAccountsManager.getCodeForNumber(SENDER_PREAUTH)).thenReturn(Optional.of(new StoredVerificationCode("555555", System.currentTimeMillis(), "validchallenge"))); when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount)); when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount)); when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty()); when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty()); + when(accountsManager.get(eq(SENDER_PREAUTH))).thenReturn(Optional.empty()); when(abusiveHostRules.getAbusiveHostRulesFor(eq(ABUSIVE_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(ABUSIVE_HOST, true, Collections.emptyList()))); when(abusiveHostRules.getAbusiveHostRulesFor(eq(RESTRICTED_HOST))).thenReturn(Collections.singletonList(new AbusiveHostRule(RESTRICTED_HOST, false, Collections.singletonList("+123")))); @@ -143,6 +156,45 @@ public class AccountControllerTest { doThrow(new RateLimitExceededException(RATE_LIMITED_HOST2)).when(smsVoiceIpLimiter).validate(RATE_LIMITED_HOST2); } + @Test + public void testGetFcmPreauth() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/accounts/fcm/preauth/mytoken/+14152222222") + .request() + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + ArgumentCaptor captor = ArgumentCaptor.forClass(GcmMessage.class); + + verify(gcmSender, times(1)).sendMessage(captor.capture()); + assertThat(captor.getValue().getGcmId()).isEqualTo("mytoken"); + assertThat(captor.getValue().getData().isPresent()).isTrue(); + assertThat(captor.getValue().getData().get().length()).isEqualTo(32); + + verifyNoMoreInteractions(apnSender); + } + + @Test + public void testGetApnPreauth() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/accounts/apn/preauth/mytoken/+14152222222") + .request() + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ApnMessage.class); + + verify(apnSender, times(1)).sendMessage(captor.capture()); + assertThat(captor.getValue().getApnId()).isEqualTo("mytoken"); + assertThat(captor.getValue().getChallengeData().isPresent()).isTrue(); + assertThat(captor.getValue().getChallengeData().get().length()).isEqualTo(32); + assertThat(captor.getValue().getMessage()).contains("\"challenge\" : \"" + captor.getValue().getChallengeData().get() + "\""); + + verifyNoMoreInteractions(gcmSender); + } + @Test public void testSendCode() throws Exception { Response response = @@ -157,7 +209,55 @@ public class AccountControllerTest { verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString()); verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); } - + + @Test + public void testSendCodeWithValidPreauth() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) + .queryParam("challenge", "validchallenge") + .request() + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(smsSender).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString()); + verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); + } + + @Test + public void testSendCodeWithInvalidPreauth() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) + .queryParam("challenge", "invalidchallenge") + .request() + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(402); + + verifyNoMoreInteractions(smsSender); + verifyNoMoreInteractions(abusiveHostRules); + } + + @Test + public void testSendCodeWithNoPreauth() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/sms/code/%s", SENDER_PREAUTH)) + .request() + .header("X-Forwarded-For", NICE_HOST) + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + + verify(smsSender).deliverSmsVerification(eq(SENDER_PREAUTH), eq(Optional.empty()), anyString()); + verify(abusiveHostRules).getAbusiveHostRulesFor(eq(NICE_HOST)); + } + + @Test public void testSendiOSCode() throws Exception { Response response = diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index 63a9e6682..362192897 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -117,8 +117,8 @@ public class DeviceControllerTest { when(account.getAuthenticatedDevice()).thenReturn(Optional.of(masterDevice)); when(account.isEnabled()).thenReturn(false); - when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis()))); - when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(new StoredVerificationCode("1112223", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31)))); + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis(), null))); + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(new StoredVerificationCode("1112223", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31), null))); when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java index a913be98d..3711fe105 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/APNSenderTest.java @@ -63,7 +63,7 @@ public class APNSenderTest { .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(executor, invocationOnMock.getArgument(0), response)); RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -75,7 +75,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); assertThat(notification.getValue().getTopic()).isEqualTo("foo.voip"); @@ -97,7 +97,7 @@ public class APNSenderTest { .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(executor, invocationOnMock.getArgument(0), response)); RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, false); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, false, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -109,7 +109,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); assertThat(notification.getValue().getTopic()).isEqualTo("foo"); @@ -133,7 +133,7 @@ public class APNSenderTest { RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -150,7 +150,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); @@ -236,7 +236,7 @@ public class APNSenderTest { .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(executor, invocationOnMock.getArgument(0), response)); RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -253,7 +253,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.NO_SUCH_USER); @@ -331,7 +331,7 @@ public class APNSenderTest { .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(executor, invocationOnMock.getArgument(0), response)); RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -343,7 +343,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); assertThat(apnResult.getStatus()).isEqualTo(ApnResult.Status.GENERIC_FAILURE); @@ -364,7 +364,7 @@ public class APNSenderTest { .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(executor, invocationOnMock.getArgument(0), new Exception("lost connection"))); RetryingApnsClient retryingApnsClient = new RetryingApnsClient(apnsClient); - ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true); + ApnMessage message = new ApnMessage(DESTINATION_APN_ID, DESTINATION_NUMBER, 1, true, Optional.empty()); APNSender apnSender = new APNSender(new SynchronousExecutorService(), accountsManager, retryingApnsClient, "foo", false); apnSender.setApnFallbackManager(fallbackManager); @@ -384,7 +384,7 @@ public class APNSenderTest { assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); assertThat(notification.getValue().getExpiration()).isEqualTo(new Date(ApnMessage.MAX_EXPIRATION)); - assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_PAYLOAD); + assertThat(notification.getValue().getPayload()).isEqualTo(ApnMessage.APN_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); verifyNoMoreInteractions(apnsClient); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/GCMSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/GCMSenderTest.java index 7f590a794..ae137587e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/GCMSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/push/GCMSenderTest.java @@ -32,7 +32,7 @@ public class GCMSenderTest { when(successResult.hasCanonicalRegistrationId()).thenReturn(false); when(successResult.isSuccess()).thenReturn(true); - GcmMessage message = new GcmMessage("foo", "+12223334444", 1, false); + GcmMessage message = new GcmMessage("foo", "+12223334444", 1, GcmMessage.Type.NOTIFICATION, Optional.empty()); GCMSender gcmSender = new GCMSender(accountsManager, sender, executorService); CompletableFuture successFuture = CompletableFuture.completedFuture(successResult); @@ -66,7 +66,7 @@ public class GCMSenderTest { when(invalidResult.hasCanonicalRegistrationId()).thenReturn(false); when(invalidResult.isSuccess()).thenReturn(true); - GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false); + GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, GcmMessage.Type.NOTIFICATION, Optional.empty()); GCMSender gcmSender = new GCMSender(accountsManager, sender, executorService); CompletableFuture invalidFuture = CompletableFuture.completedFuture(invalidResult); @@ -105,7 +105,7 @@ public class GCMSenderTest { when(canonicalResult.isSuccess()).thenReturn(false); when(canonicalResult.getCanonicalRegistrationId()).thenReturn(canonicalId); - GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, false); + GcmMessage message = new GcmMessage(gcmId, destinationNumber, 1, GcmMessage.Type.NOTIFICATION, Optional.empty()); GCMSender gcmSender = new GCMSender(accountsManager, sender, executorService); CompletableFuture invalidFuture = CompletableFuture.completedFuture(canonicalResult); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/PendingAccountsTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/PendingAccountsTest.java index 5a2644a3e..420a7ef75 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/PendingAccountsTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/storage/PendingAccountsTest.java @@ -33,7 +33,7 @@ public class PendingAccountsTest { @Test public void testStore() throws SQLException { - pendingAccounts.insert("+14151112222", "1234", 1111); + pendingAccounts.insert("+14151112222", "1234", 1111, null); PreparedStatement statement = db.getTestDatabase().getConnection().prepareStatement("SELECT * FROM pending_accounts WHERE number = ?"); statement.setString(1, "+14151112222"); @@ -43,6 +43,27 @@ public class PendingAccountsTest { if (resultSet.next()) { assertThat(resultSet.getString("verification_code")).isEqualTo("1234"); assertThat(resultSet.getLong("timestamp")).isEqualTo(1111); + assertThat(resultSet.getString("push_code")).isNull(); + } else { + throw new AssertionError("no results"); + } + + assertThat(resultSet.next()).isFalse(); + } + + @Test + public void testStoreWithPushChallenge() throws SQLException { + pendingAccounts.insert("+14151112222", null, 1111, "112233"); + + PreparedStatement statement = db.getTestDatabase().getConnection().prepareStatement("SELECT * FROM pending_accounts WHERE number = ?"); + statement.setString(1, "+14151112222"); + + ResultSet resultSet = statement.executeQuery(); + + if (resultSet.next()) { + assertThat(resultSet.getString("verification_code")).isNull(); + assertThat(resultSet.getLong("timestamp")).isEqualTo(1111); + assertThat(resultSet.getString("push_code")).isEqualTo("112233"); } else { throw new AssertionError("no results"); } @@ -52,8 +73,8 @@ public class PendingAccountsTest { @Test public void testRetrieve() throws Exception { - pendingAccounts.insert("+14151112222", "4321", 2222); - pendingAccounts.insert("+14151113333", "1212", 5555); + pendingAccounts.insert("+14151112222", "4321", 2222, null); + pendingAccounts.insert("+14151113333", "1212", 5555, null); Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); @@ -65,10 +86,26 @@ public class PendingAccountsTest { assertThat(missingCode.isPresent()).isFalse(); } + @Test + public void testRetrieveWithPushChallenge() throws Exception { + pendingAccounts.insert("+14151112222", "4321", 2222, "bar"); + pendingAccounts.insert("+14151113333", "1212", 5555, "bang"); + + Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); + + assertThat(verificationCode.isPresent()).isTrue(); + assertThat(verificationCode.get().getCode()).isEqualTo("4321"); + assertThat(verificationCode.get().getTimestamp()).isEqualTo(2222); + assertThat(verificationCode.get().getPushCode()).isEqualTo("bar"); + + Optional missingCode = pendingAccounts.getCodeForNumber("+11111111111"); + assertThat(missingCode.isPresent()).isFalse(); + } + @Test public void testOverwrite() throws Exception { - pendingAccounts.insert("+14151112222", "4321", 2222); - pendingAccounts.insert("+14151112222", "4444", 3333); + pendingAccounts.insert("+14151112222", "4321", 2222, null); + pendingAccounts.insert("+14151112222", "4444", 3333, null); Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); @@ -77,10 +114,24 @@ public class PendingAccountsTest { assertThat(verificationCode.get().getTimestamp()).isEqualTo(3333); } + @Test + public void testOverwriteWithPushToken() throws Exception { + pendingAccounts.insert("+14151112222", "4321", 2222, "bar"); + pendingAccounts.insert("+14151112222", "4444", 3333, "bang"); + + Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); + + assertThat(verificationCode.isPresent()).isTrue(); + assertThat(verificationCode.get().getCode()).isEqualTo("4444"); + assertThat(verificationCode.get().getTimestamp()).isEqualTo(3333); + assertThat(verificationCode.get().getPushCode()).isEqualTo("bang"); + } + + @Test public void testVacuum() { - pendingAccounts.insert("+14151112222", "4321", 2222); - pendingAccounts.insert("+14151112222", "4444", 3333); + pendingAccounts.insert("+14151112222", "4321", 2222, null); + pendingAccounts.insert("+14151112222", "4444", 3333, null); pendingAccounts.vacuum(); Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); @@ -92,8 +143,8 @@ public class PendingAccountsTest { @Test public void testRemove() { - pendingAccounts.insert("+14151112222", "4321", 2222); - pendingAccounts.insert("+14151113333", "1212", 5555); + pendingAccounts.insert("+14151112222", "4321", 2222, "bar"); + pendingAccounts.insert("+14151113333", "1212", 5555, null); Optional verificationCode = pendingAccounts.getCodeForNumber("+14151112222"); @@ -110,6 +161,7 @@ public class PendingAccountsTest { assertThat(verificationCode.isPresent()).isTrue(); assertThat(verificationCode.get().getCode()).isEqualTo("1212"); assertThat(verificationCode.get().getTimestamp()).isEqualTo(5555); + assertThat(verificationCode.get().getPushCode()).isNull(); }