Skip to content

Commit

Permalink
Support requesting a CAPTCHA during registration.
Browse files Browse the repository at this point in the history
  • Loading branch information
greyson-signal committed Feb 15, 2019
1 parent 2cfa431 commit 02b0800
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 30 deletions.
6 changes: 6 additions & 0 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

<activity android:name=".registration.CaptchaActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

<activity android:name=".DeviceActivity"
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
Expand Down
41 changes: 41 additions & 0 deletions res/layout/captcha_activity.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">


<TextView
android:id="@+id/registration_captcha_title"
style="@style/Signal.Text.Headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:text="@string/RegistrationActivity_we_need_to_verify_that_youre_human"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<WebView
android:id="@+id/registration_captcha_web_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/registration_captcha_title" />

</android.support.constraint.ConstraintLayout>
2 changes: 2 additions & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@
<item quantity="one">You are now %d step away from submitting a debug log.</item>
<item quantity="other">You are now %d steps away from submitting a debug log.</item>
</plurals>
<string name="RegistrationActivity_we_need_to_verify_that_youre_human">We need to verify that you\'re human.</string>
<string name="RegistrationActivity_failed_to_verify_the_captcha">Failed to verify the CAPTCHA</string>

<!-- ScribbleActivity -->
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>
Expand Down
104 changes: 74 additions & 30 deletions src/org/thoughtcrime/securesms/RegistrationActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.VerificationCodeParser;
Expand All @@ -96,6 +97,7 @@
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import org.whispersystems.signalservice.internal.push.LockedException;
Expand All @@ -117,6 +119,7 @@
public class RegistrationActivity extends BaseActionBarActivity implements VerificationCodeView.OnCodeEnteredListener {

private static final int PICK_COUNTRY = 1;
private static final int CAPTCHA = 24601;
private static final int SCENE_TRANSITION_DURATION = 250;
private static final int DEBUG_TAP_TARGET = 8;
private static final int DEBUG_TAP_ANNOUNCE = 4;
Expand Down Expand Up @@ -185,6 +188,16 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
this.countryCode.setText(String.valueOf(data.getIntExtra("country_code", 1)));
setCountryDisplay(data.getStringExtra("country_name"));
setCountryFormatter(data.getIntExtra("country_code", 1));
} else if (requestCode == CAPTCHA && resultCode == RESULT_OK && data != null) {
registrationState = new RegistrationState(Optional.fromNullable(data.getStringExtra(CaptchaActivity.KEY_TOKEN)), registrationState);

if (data.getBooleanExtra(CaptchaActivity.KEY_IS_SMS, true)) {
handleRegister();
} else {
handlePhoneCallRequest();
}
} else if (requestCode == CAPTCHA) {
Toast.makeText(this, R.string.RegistrationActivity_failed_to_verify_the_captcha, Toast.LENGTH_LONG).show();
}
}

Expand Down Expand Up @@ -226,7 +239,7 @@ private void initializeResources() {
this.pinForgotButton = findViewById(R.id.forgot_button);
this.pinClarificationContainer = findViewById(R.id.pin_clarification_container);

this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());

this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
this.number.addTextChangedListener(new NumberChangedListener());
Expand Down Expand Up @@ -388,13 +401,14 @@ private void handleRestore(BackupUtil.BackupInfo backup) {
restoreButton.setIndeterminateProgressMode(true);
restoreButton.setProgress(50);

final String passphrase = prompt.getText().toString();

new AsyncTask<Void, Void, BackupImportResult>() {
@Override
protected BackupImportResult doInBackground(Void... voids) {
try {
Context context = RegistrationActivity.this;
String passphrase = prompt.getText().toString();
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
Context context = RegistrationActivity.this;
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);

FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
Expand Down Expand Up @@ -498,9 +512,9 @@ private void handleRequestVerification(@NonNull String e164number, boolean gcmSu

@SuppressLint("StaticFieldLeak")
private void requestVerificationCode(@NonNull String e164number, boolean gcmSupported, boolean smsRetrieverSupported) {
new AsyncTask<Void, Void, Pair<String, Optional<String>>> () {
new AsyncTask<Void, Void, VerificationRequestResult> () {
@Override
protected @Nullable Pair<String, Optional<String>> doInBackground(Void... voids) {
protected @NonNull VerificationRequestResult doInBackground(Void... voids) {
try {
markAsVerifying(true);

Expand All @@ -515,29 +529,34 @@ private void requestVerificationCode(@NonNull String e164number, boolean gcmSupp
}

accountManager = AccountManagerFactory.createManager(RegistrationActivity.this, e164number, password);
accountManager.requestSmsVerificationCode(smsRetrieverSupported);
accountManager.requestSmsVerificationCode(smsRetrieverSupported, registrationState.captchaToken);

return new Pair<>(password, fcmToken);
return new VerificationRequestResult(password, fcmToken, Optional.absent());
} catch (IOException e) {
Log.w(TAG, "Error during account registration", e);
return null;
return new VerificationRequestResult(null, Optional.absent(), Optional.of(e));
}
}

protected void onPostExecute(@Nullable Pair<String, Optional<String>> result) {
if (result == null) {
protected void onPostExecute(@NonNull VerificationRequestResult result) {
if (result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException) {
requestCaptcha(true);
} else if (result.exception.isPresent()) {
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
createButton.setIndeterminateProgressMode(false);
createButton.setProgress(0);
return;
} else {
registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.password, result.fcmToken, Optional.absent());
displayVerificationView(e164number, 64);
}

registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.first, result.second);
displayVerificationView(e164number, 64);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}

private void requestCaptcha(boolean isSms) {
startActivityForResult(CaptchaActivity.getIntent(this, isSms), CAPTCHA);
}

private void handleVerificationCodeReceived(@Nullable String code) {
List<Integer> parsedCode = convertVerificationCodeToDigits(code);

Expand Down Expand Up @@ -693,7 +712,9 @@ private void handlePhoneCallRequest() {
@Override
protected Void doInBackground(Void... voids) {
try {
accountManager.requestVoiceVerificationCode(Locale.getDefault());
accountManager.requestVoiceVerificationCode(Locale.getDefault(), registrationState.captchaToken);
} catch (CaptchaRequiredException e) {
requestCaptcha(false);
} catch (IOException e) {
Log.w(TAG, e);
}
Expand Down Expand Up @@ -892,7 +913,7 @@ public void onAnimationEnd(Animator animation) {
@Override
public void onClick(View widget) {
displayInitialView(false);
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());
}

@Override
Expand Down Expand Up @@ -1157,28 +1178,51 @@ public void onClick(View v) {
}
}

private static class VerificationRequestResult {
private final String password;
private final Optional<String> fcmToken;
private final Optional<IOException> exception;

private VerificationRequestResult(String password, Optional<String> fcmToken, Optional<IOException> exception) {
this.password = password;
this.fcmToken = fcmToken;
this.exception = exception;
}
}

private static class RegistrationState {
private enum State {
INITIAL, VERIFYING, CHECKING, PIN
}

private final State state;
private final String e164number;
private final String password;
private final State state;
private final String e164number;
private final String password;
private final Optional<String> gcmToken;

RegistrationState(State state, String e164number, String password, Optional<String> gcmToken) {
this.state = state;
this.e164number = e164number;
this.password = password;
this.gcmToken = gcmToken;
private final Optional<String> captchaToken;

RegistrationState(State state, String e164number, String password, Optional<String> gcmToken, Optional<String> captchaToken) {
this.state = state;
this.e164number = e164number;
this.password = password;
this.gcmToken = gcmToken;
this.captchaToken = captchaToken;
}

RegistrationState(State state, RegistrationState previous) {
this.state = state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.state = state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.captchaToken = previous.captchaToken;
}

RegistrationState(Optional<String> captchaToken, RegistrationState previous) {
this.state = previous.state;
this.e164number = previous.e164number;
this.password = previous.password;
this.gcmToken = previous.gcmToken;
this.captchaToken = captchaToken;
}
}

Expand Down
65 changes: 65 additions & 0 deletions src/org/thoughtcrime/securesms/registration/CaptchaActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.registration;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import org.thoughtcrime.securesms.BaseActionBarActivity;
import org.thoughtcrime.securesms.R;

public class CaptchaActivity extends BaseActionBarActivity {

public static final String KEY_TOKEN = "token";
public static final String KEY_IS_SMS = "is_sms";

private static final String SIGNAL_SCHEME = "signalcaptcha://";

public static Intent getIntent(@NonNull Context context, boolean isSms) {
Intent intent = new Intent(context, CaptchaActivity.class);
intent.putExtra(KEY_IS_SMS, isSms);
return intent;
}

@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.captcha_activity);

WebView webView = findViewById(R.id.registration_captcha_web_view);

webView.getSettings().setJavaScriptEnabled(true);
webView.clearCache(true);

webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(SIGNAL_SCHEME)) {
handleToken(url.substring(SIGNAL_SCHEME.length()));
return true;
}
return false;
}
});

webView.loadUrl("https://signalcaptchas.org/registration/generate.html\n");
}

public void handleToken(String token) {
if (!TextUtils.isEmpty(token)) {
Intent result = new Intent();
result.putExtra(KEY_TOKEN, token);
result.putExtra(KEY_IS_SMS, getIntent().getBooleanExtra(KEY_IS_SMS, true));
setResult(RESULT_OK, result);
} else {
setResult(RESULT_CANCELED);
}

finish();
}
}

1 comment on commit 02b0800

@lukefromdc
Copy link

@lukefromdc lukefromdc commented on 02b0800 Feb 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit breaks the expectation of privacy when Google Play Services is disabled and exposes users to potential identification by a subpeona to Google for all Recaptcha requests from Signal registrations. #10999

Please sign in to comment.