Request a push challenge and supply to SMS and voice verification.

This commit is contained in:
Alan Evans 2019-07-10 16:59:08 -04:00 committed by GitHub
parent d72d4c4c41
commit 5b61c8ac18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 281 additions and 10 deletions

View file

@ -9,9 +9,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@ -28,6 +25,10 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.dd.CircularProgressButton;
import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.registration.CaptchaActivity;
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.VerificationCodeParser;
@ -115,7 +117,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
private static final int SCENE_TRANSITION_DURATION = 250;
private static final int DEBUG_TAP_TARGET = 8;
private static final int DEBUG_TAP_ANNOUNCE = 4;
public static final String RE_REGISTRATION_EXTRA = "re_registration";
private static final long PUSH_REQUEST_TIMEOUT_MS = 5000L;
public static final String RE_REGISTRATION_EXTRA = "re_registration";
private static final String TAG = RegistrationActivity.class.getSimpleName();
@ -492,7 +496,10 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
accountManager = AccountManagerFactory.createManager(RegistrationActivity.this, e164number, password);
accountManager.requestSmsVerificationCode(smsRetrieverSupported, registrationState.captchaToken, Optional.absent());
Optional<String> pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164number, PUSH_REQUEST_TIMEOUT_MS);
accountManager.requestSmsVerificationCode(smsRetrieverSupported, registrationState.captchaToken, pushChallenge);
return new VerificationRequestResult(password, fcmToken, Optional.absent());
} catch (IOException e) {
@ -668,6 +675,8 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@SuppressLint("StaticFieldLeak")
private void handlePhoneCallRequest() {
final String e164number = getConfiguredE164Number();
if (registrationState.state == RegistrationState.State.VERIFYING) {
callMeCountDownView.startCountDown(300);
@ -675,7 +684,9 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
@Override
protected Void doInBackground(Void... voids) {
try {
accountManager.requestVoiceVerificationCode(Locale.getDefault(), registrationState.captchaToken, Optional.absent());
Optional<String> pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, getFcmToken(), e164number, PUSH_REQUEST_TIMEOUT_MS);
accountManager.requestVoiceVerificationCode(Locale.getDefault(), registrationState.captchaToken, pushChallenge);
} catch (CaptchaRequiredException e) {
requestCaptcha(false);
} catch (IOException e) {
@ -688,6 +699,15 @@ public class RegistrationActivity extends BaseActionBarActivity implements Verif
}
}
private Optional<String> getFcmToken() {
final boolean gcmSupported = PlayServicesUtil.getPlayServicesStatus(this) == PlayServicesStatus.SUCCESS;
if (gcmSupported) {
return FcmUtil.getToken();
} else {
return Optional.absent();
}
}
private void verifyAccount(@NonNull String code, @Nullable String pin) throws IOException {
int registrationId = KeyHelper.generateRegistrationId(false);
byte[] unidentifiedAccessKey = UnidentifiedAccessUtil.getSelfUnidentifiedAccessKey(RegistrationActivity.this);

View file

@ -4,6 +4,8 @@ import android.content.Context;
import android.os.Build;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
@ -13,6 +15,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
import org.thoughtcrime.securesms.util.PowerManagerCompat;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -38,11 +41,17 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.i(TAG, "FCM message... Original Priority: " + remoteMessage.getOriginalPriority() + ", Actual Priority: " + remoteMessage.getPriority());
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
WakeLockUtil.runWithLock(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK, 60000, WAKE_LOCK_TAG, () -> {
handleReceivedNotification(getApplicationContext());
});
String challenge = remoteMessage.getData().get("challenge");
if (challenge != null) {
handlePushChallenge(challenge);
} else {
ApplicationContext.getInstance(getApplicationContext()).injectDependencies(this);
WakeLockUtil.runWithLock(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK, 60000, WAKE_LOCK_TAG, () ->
handleReceivedNotification(getApplicationContext())
);
}
}
@Override
@ -95,6 +104,12 @@ public class FcmService extends FirebaseMessagingService implements InjectableTy
Log.i(TAG, "Processing complete.");
}
private static void handlePushChallenge(@NonNull String challenge) {
Log.d(TAG, String.format("Got a push challenge \"%s\"", challenge));
PushChallengeRequest.postChallengeResponse(challenge);
}
private static synchronized boolean incrementActiveGcmCount() {
if (activeCount < 2) {
activeCount++;

View file

@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.registration;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public final class PushChallengeRequest {
private static final String TAG = Log.tag(PushChallengeRequest.class);
/**
* Requests a push challenge and waits for the response.
* <p>
* Blocks the current thread for up to {@param timeoutMs} milliseconds.
*
* @param accountManager Account manager to request the push from.
* @param fcmToken Optional FCM token. If not present will return absent.
* @param e164number Local number.
* @param timeoutMs Timeout in milliseconds
* @return Either returns a challenge, or absent.
*/
@WorkerThread
public static Optional<String> getPushChallengeBlocking(@NonNull SignalServiceAccountManager accountManager,
@NonNull Optional<String> fcmToken,
@NonNull String e164number,
long timeoutMs)
{
if (!fcmToken.isPresent()) {
Log.w(TAG, "Push challenge not requested, as no FCM token was present");
return Optional.absent();
}
long startTime = System.currentTimeMillis();
Log.i(TAG, "Requesting a push challenge");
Request request = new Request(accountManager, fcmToken.get(), e164number, timeoutMs);
Optional<String> challenge = request.requestAndReceiveChallengeBlocking();
long duration = System.currentTimeMillis() - startTime;
if (challenge.isPresent()) {
Log.i(TAG, String.format(Locale.US, "Received a push challenge \"%s\" in %d ms", challenge.get(), duration));
} else {
Log.w(TAG, String.format(Locale.US, "Did not received a push challenge in %d ms", duration));
}
return challenge;
}
public static void postChallengeResponse(@NonNull String challenge) {
EventBus.getDefault().post(new PushChallengeEvent(challenge));
}
private static class Request {
private final CountDownLatch latch;
private final AtomicReference<String> challenge;
private final SignalServiceAccountManager accountManager;
private final String fcmToken;
private final String e164number;
private final long timeoutMs;
private Request(@NonNull SignalServiceAccountManager accountManager,
@NonNull String fcmToken,
@NonNull String e164number,
long timeoutMs)
{
this.latch = new CountDownLatch(1);
this.challenge = new AtomicReference<>();
this.accountManager = accountManager;
this.fcmToken = fcmToken;
this.e164number = e164number;
this.timeoutMs = timeoutMs;
}
@WorkerThread
private Optional<String> requestAndReceiveChallengeBlocking() {
EventBus eventBus = EventBus.getDefault();
eventBus.register(this);
try {
accountManager.requestPushChallenge(fcmToken, e164number);
latch.await(timeoutMs, TimeUnit.MILLISECONDS);
return Optional.fromNullable(challenge.get());
} catch (InterruptedException | IOException e) {
Log.w(TAG, "Error getting push challenge", e);
return Optional.absent();
} finally {
eventBus.unregister(this);
}
}
@Subscribe(threadMode = ThreadMode.POSTING)
public void onChallengeEvent(@NonNull PushChallengeEvent pushChallengeEvent) {
challenge.set(pushChallengeEvent.challenge);
latch.countDown();
}
}
static class PushChallengeEvent {
private final String challenge;
PushChallengeEvent(String challenge) {
this.challenge = challenge;
}
}
}

View file

@ -0,0 +1,115 @@
package org.thoughtcrime.securesms.registration;
import android.app.Application;
import android.os.AsyncTask;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import java.io.IOException;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, application = Application.class)
public final class PushChallengeRequestTest {
@Test
public void getPushChallengeBlocking_returns_absent_if_times_out() {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
Optional<String> challenge = PushChallengeRequest.getPushChallengeBlocking(signal, Optional.of("token"), "+123456", 50L);
assertFalse(challenge.isPresent());
}
@Test
public void getPushChallengeBlocking_waits_for_specified_period() {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
long startTime = System.currentTimeMillis();
PushChallengeRequest.getPushChallengeBlocking(signal, Optional.of("token"), "+123456", 250L);
long duration = System.currentTimeMillis() - startTime;
assertThat(duration, greaterThanOrEqualTo(250L));
}
@Test
public void getPushChallengeBlocking_completes_fast_if_posted_to_event_bus() throws IOException {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
doAnswer(invocation -> {
AsyncTask.execute(() -> PushChallengeRequest.postChallengeResponse("CHALLENGE"));
return null;
}).when(signal).requestPushChallenge("token", "+123456");
long startTime = System.currentTimeMillis();
Optional<String> challenge = PushChallengeRequest.getPushChallengeBlocking(signal, Optional.of("token"), "+123456", 500L);
long duration = System.currentTimeMillis() - startTime;
assertThat(duration, lessThan(500L));
verify(signal).requestPushChallenge("token", "+123456");
verifyNoMoreInteractions(signal);
assertTrue(challenge.isPresent());
assertEquals("CHALLENGE", challenge.get());
}
@Test
public void getPushChallengeBlocking_returns_fast_if_no_fcm_token_supplied() {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
long startTime = System.currentTimeMillis();
PushChallengeRequest.getPushChallengeBlocking(signal, Optional.absent(), "+123456", 500L);
long duration = System.currentTimeMillis() - startTime;
assertThat(duration, lessThan(500L));
}
@Test
public void getPushChallengeBlocking_returns_absent_if_no_fcm_token_supplied() {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
Optional<String> challenge = PushChallengeRequest.getPushChallengeBlocking(signal, Optional.absent(), "+123456", 500L);
verifyZeroInteractions(signal);
assertFalse(challenge.isPresent());
}
@Test
public void getPushChallengeBlocking_returns_absent_if_any_IOException_is_thrown() throws IOException {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
doThrow(new IOException()).when(signal).requestPushChallenge(any(), any());
Optional<String> challenge = PushChallengeRequest.getPushChallengeBlocking(signal, Optional.of("token"), "+123456", 500L);
assertFalse(challenge.isPresent());
}
@Test
public void getPushChallengeBlocking_returns_absent_if_any_RuntimeException_is_thrown() throws IOException {
SignalServiceAccountManager signal = mock(SignalServiceAccountManager.class);
doThrow(new RuntimeException()).when(signal).requestPushChallenge(any(), any());
Optional<String> challenge = PushChallengeRequest.getPushChallengeBlocking(signal, Optional.of("token"), "+123456", 500L);
assertFalse(challenge.isPresent());
}
}