Skip to content

Commit 02b0800

Browse files
Support requesting a CAPTCHA during registration.
1 parent 2cfa431 commit 02b0800

File tree

5 files changed

+188
-30
lines changed

5 files changed

+188
-30
lines changed

AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,12 @@
305305
android:windowSoftInputMode="stateUnchanged"
306306
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
307307

308+
<activity android:name=".registration.CaptchaActivity"
309+
android:launchMode="singleTask"
310+
android:theme="@style/TextSecure.LightNoActionBar"
311+
android:windowSoftInputMode="stateUnchanged"
312+
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
313+
308314
<activity android:name=".DeviceActivity"
309315
android:label="@string/AndroidManifest__linked_devices"
310316
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

res/layout/captcha_activity.xml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<android.support.constraint.ConstraintLayout
3+
xmlns:android="http://schemas.android.com/apk/res/android"
4+
xmlns:app="http://schemas.android.com/apk/res-auto"
5+
xmlns:tools="http://schemas.android.com/tools"
6+
android:layout_width="match_parent"
7+
android:layout_height="match_parent">
8+
9+
10+
<TextView
11+
android:id="@+id/registration_captcha_title"
12+
style="@style/Signal.Text.Headline"
13+
android:layout_width="0dp"
14+
android:layout_height="wrap_content"
15+
android:layout_marginStart="24dp"
16+
android:layout_marginLeft="24dp"
17+
android:layout_marginTop="24dp"
18+
android:layout_marginEnd="24dp"
19+
android:layout_marginRight="24dp"
20+
android:text="@string/RegistrationActivity_we_need_to_verify_that_youre_human"
21+
android:gravity="center"
22+
app:layout_constraintEnd_toEndOf="parent"
23+
app:layout_constraintStart_toStartOf="parent"
24+
app:layout_constraintTop_toTopOf="parent" />
25+
26+
<WebView
27+
android:id="@+id/registration_captcha_web_view"
28+
android:layout_width="0dp"
29+
android:layout_height="0dp"
30+
android:layout_marginStart="16dp"
31+
android:layout_marginLeft="16dp"
32+
android:layout_marginTop="16dp"
33+
android:layout_marginEnd="16dp"
34+
android:layout_marginRight="16dp"
35+
android:layout_marginBottom="16dp"
36+
app:layout_constraintBottom_toBottomOf="parent"
37+
app:layout_constraintEnd_toEndOf="parent"
38+
app:layout_constraintStart_toStartOf="parent"
39+
app:layout_constraintTop_toBottomOf="@+id/registration_captcha_title" />
40+
41+
</android.support.constraint.ConstraintLayout>

res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,8 @@
597597
<item quantity="one">You are now %d step away from submitting a debug log.</item>
598598
<item quantity="other">You are now %d steps away from submitting a debug log.</item>
599599
</plurals>
600+
<string name="RegistrationActivity_we_need_to_verify_that_youre_human">We need to verify that you\'re human.</string>
601+
<string name="RegistrationActivity_failed_to_verify_the_captcha">Failed to verify the CAPTCHA</string>
600602

601603
<!-- ScribbleActivity -->
602604
<string name="ScribbleActivity_save_failure">Failed to save image changes</string>

src/org/thoughtcrime/securesms/RegistrationActivity.java

Lines changed: 74 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
import org.thoughtcrime.securesms.notifications.NotificationChannels;
7979
import org.thoughtcrime.securesms.permissions.Permissions;
8080
import org.thoughtcrime.securesms.push.AccountManagerFactory;
81+
import org.thoughtcrime.securesms.registration.CaptchaActivity;
8182
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
8283
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
8384
import org.thoughtcrime.securesms.service.VerificationCodeParser;
@@ -96,6 +97,7 @@
9697
import org.whispersystems.libsignal.util.KeyHelper;
9798
import org.whispersystems.libsignal.util.guava.Optional;
9899
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
100+
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
99101
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
100102
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
101103
import org.whispersystems.signalservice.internal.push.LockedException;
@@ -117,6 +119,7 @@
117119
public class RegistrationActivity extends BaseActionBarActivity implements VerificationCodeView.OnCodeEnteredListener {
118120

119121
private static final int PICK_COUNTRY = 1;
122+
private static final int CAPTCHA = 24601;
120123
private static final int SCENE_TRANSITION_DURATION = 250;
121124
private static final int DEBUG_TAP_TARGET = 8;
122125
private static final int DEBUG_TAP_ANNOUNCE = 4;
@@ -185,6 +188,16 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
185188
this.countryCode.setText(String.valueOf(data.getIntExtra("country_code", 1)));
186189
setCountryDisplay(data.getStringExtra("country_name"));
187190
setCountryFormatter(data.getIntExtra("country_code", 1));
191+
} else if (requestCode == CAPTCHA && resultCode == RESULT_OK && data != null) {
192+
registrationState = new RegistrationState(Optional.fromNullable(data.getStringExtra(CaptchaActivity.KEY_TOKEN)), registrationState);
193+
194+
if (data.getBooleanExtra(CaptchaActivity.KEY_IS_SMS, true)) {
195+
handleRegister();
196+
} else {
197+
handlePhoneCallRequest();
198+
}
199+
} else if (requestCode == CAPTCHA) {
200+
Toast.makeText(this, R.string.RegistrationActivity_failed_to_verify_the_captcha, Toast.LENGTH_LONG).show();
188201
}
189202
}
190203

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

229-
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
242+
this.registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());
230243

231244
this.countryCode.addTextChangedListener(new CountryCodeChangedListener());
232245
this.number.addTextChangedListener(new NumberChangedListener());
@@ -388,13 +401,14 @@ private void handleRestore(BackupUtil.BackupInfo backup) {
388401
restoreButton.setIndeterminateProgressMode(true);
389402
restoreButton.setProgress(50);
390403

404+
final String passphrase = prompt.getText().toString();
405+
391406
new AsyncTask<Void, Void, BackupImportResult>() {
392407
@Override
393408
protected BackupImportResult doInBackground(Void... voids) {
394409
try {
395-
Context context = RegistrationActivity.this;
396-
String passphrase = prompt.getText().toString();
397-
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
410+
Context context = RegistrationActivity.this;
411+
SQLiteDatabase database = DatabaseFactory.getBackupDatabase(context);
398412

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

499513
@SuppressLint("StaticFieldLeak")
500514
private void requestVerificationCode(@NonNull String e164number, boolean gcmSupported, boolean smsRetrieverSupported) {
501-
new AsyncTask<Void, Void, Pair<String, Optional<String>>> () {
515+
new AsyncTask<Void, Void, VerificationRequestResult> () {
502516
@Override
503-
protected @Nullable Pair<String, Optional<String>> doInBackground(Void... voids) {
517+
protected @NonNull VerificationRequestResult doInBackground(Void... voids) {
504518
try {
505519
markAsVerifying(true);
506520

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

517531
accountManager = AccountManagerFactory.createManager(RegistrationActivity.this, e164number, password);
518-
accountManager.requestSmsVerificationCode(smsRetrieverSupported);
532+
accountManager.requestSmsVerificationCode(smsRetrieverSupported, registrationState.captchaToken);
519533

520-
return new Pair<>(password, fcmToken);
534+
return new VerificationRequestResult(password, fcmToken, Optional.absent());
521535
} catch (IOException e) {
522536
Log.w(TAG, "Error during account registration", e);
523-
return null;
537+
return new VerificationRequestResult(null, Optional.absent(), Optional.of(e));
524538
}
525539
}
526540

527-
protected void onPostExecute(@Nullable Pair<String, Optional<String>> result) {
528-
if (result == null) {
541+
protected void onPostExecute(@NonNull VerificationRequestResult result) {
542+
if (result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException) {
543+
requestCaptcha(true);
544+
} else if (result.exception.isPresent()) {
529545
Toast.makeText(RegistrationActivity.this, R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
530546
createButton.setIndeterminateProgressMode(false);
531547
createButton.setProgress(0);
532-
return;
548+
} else {
549+
registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.password, result.fcmToken, Optional.absent());
550+
displayVerificationView(e164number, 64);
533551
}
534-
535-
registrationState = new RegistrationState(RegistrationState.State.VERIFYING, e164number, result.first, result.second);
536-
displayVerificationView(e164number, 64);
537552
}
538553
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
539554
}
540555

556+
private void requestCaptcha(boolean isSms) {
557+
startActivityForResult(CaptchaActivity.getIntent(this, isSms), CAPTCHA);
558+
}
559+
541560
private void handleVerificationCodeReceived(@Nullable String code) {
542561
List<Integer> parsedCode = convertVerificationCodeToDigits(code);
543562

@@ -693,7 +712,9 @@ private void handlePhoneCallRequest() {
693712
@Override
694713
protected Void doInBackground(Void... voids) {
695714
try {
696-
accountManager.requestVoiceVerificationCode(Locale.getDefault());
715+
accountManager.requestVoiceVerificationCode(Locale.getDefault(), registrationState.captchaToken);
716+
} catch (CaptchaRequiredException e) {
717+
requestCaptcha(false);
697718
} catch (IOException e) {
698719
Log.w(TAG, e);
699720
}
@@ -892,7 +913,7 @@ public void onAnimationEnd(Animator animation) {
892913
@Override
893914
public void onClick(View widget) {
894915
displayInitialView(false);
895-
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, null);
916+
registrationState = new RegistrationState(RegistrationState.State.INITIAL, null, null, Optional.absent(), Optional.absent());
896917
}
897918

898919
@Override
@@ -1157,28 +1178,51 @@ public void onClick(View v) {
11571178
}
11581179
}
11591180

1181+
private static class VerificationRequestResult {
1182+
private final String password;
1183+
private final Optional<String> fcmToken;
1184+
private final Optional<IOException> exception;
1185+
1186+
private VerificationRequestResult(String password, Optional<String> fcmToken, Optional<IOException> exception) {
1187+
this.password = password;
1188+
this.fcmToken = fcmToken;
1189+
this.exception = exception;
1190+
}
1191+
}
1192+
11601193
private static class RegistrationState {
11611194
private enum State {
11621195
INITIAL, VERIFYING, CHECKING, PIN
11631196
}
11641197

1165-
private final State state;
1166-
private final String e164number;
1167-
private final String password;
1198+
private final State state;
1199+
private final String e164number;
1200+
private final String password;
11681201
private final Optional<String> gcmToken;
1169-
1170-
RegistrationState(State state, String e164number, String password, Optional<String> gcmToken) {
1171-
this.state = state;
1172-
this.e164number = e164number;
1173-
this.password = password;
1174-
this.gcmToken = gcmToken;
1202+
private final Optional<String> captchaToken;
1203+
1204+
RegistrationState(State state, String e164number, String password, Optional<String> gcmToken, Optional<String> captchaToken) {
1205+
this.state = state;
1206+
this.e164number = e164number;
1207+
this.password = password;
1208+
this.gcmToken = gcmToken;
1209+
this.captchaToken = captchaToken;
11751210
}
11761211

11771212
RegistrationState(State state, RegistrationState previous) {
1178-
this.state = state;
1179-
this.e164number = previous.e164number;
1180-
this.password = previous.password;
1181-
this.gcmToken = previous.gcmToken;
1213+
this.state = state;
1214+
this.e164number = previous.e164number;
1215+
this.password = previous.password;
1216+
this.gcmToken = previous.gcmToken;
1217+
this.captchaToken = previous.captchaToken;
1218+
}
1219+
1220+
RegistrationState(Optional<String> captchaToken, RegistrationState previous) {
1221+
this.state = previous.state;
1222+
this.e164number = previous.e164number;
1223+
this.password = previous.password;
1224+
this.gcmToken = previous.gcmToken;
1225+
this.captchaToken = captchaToken;
11821226
}
11831227
}
11841228

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package org.thoughtcrime.securesms.registration;
2+
3+
import android.annotation.SuppressLint;
4+
import android.content.Context;
5+
import android.content.Intent;
6+
import android.os.Bundle;
7+
import android.support.annotation.NonNull;
8+
import android.text.TextUtils;
9+
import android.webkit.WebView;
10+
import android.webkit.WebViewClient;
11+
12+
import org.thoughtcrime.securesms.BaseActionBarActivity;
13+
import org.thoughtcrime.securesms.R;
14+
15+
public class CaptchaActivity extends BaseActionBarActivity {
16+
17+
public static final String KEY_TOKEN = "token";
18+
public static final String KEY_IS_SMS = "is_sms";
19+
20+
private static final String SIGNAL_SCHEME = "signalcaptcha://";
21+
22+
public static Intent getIntent(@NonNull Context context, boolean isSms) {
23+
Intent intent = new Intent(context, CaptchaActivity.class);
24+
intent.putExtra(KEY_IS_SMS, isSms);
25+
return intent;
26+
}
27+
28+
@SuppressLint("SetJavaScriptEnabled")
29+
@Override
30+
protected void onCreate(Bundle savedInstanceState) {
31+
super.onCreate(savedInstanceState);
32+
setContentView(R.layout.captcha_activity);
33+
34+
WebView webView = findViewById(R.id.registration_captcha_web_view);
35+
36+
webView.getSettings().setJavaScriptEnabled(true);
37+
webView.clearCache(true);
38+
39+
webView.setWebViewClient(new WebViewClient() {
40+
@Override
41+
public boolean shouldOverrideUrlLoading(WebView view, String url) {
42+
if (url != null && url.startsWith(SIGNAL_SCHEME)) {
43+
handleToken(url.substring(SIGNAL_SCHEME.length()));
44+
return true;
45+
}
46+
return false;
47+
}
48+
});
49+
50+
webView.loadUrl("https://signalcaptchas.org/registration/generate.html\n");
51+
}
52+
53+
public void handleToken(String token) {
54+
if (!TextUtils.isEmpty(token)) {
55+
Intent result = new Intent();
56+
result.putExtra(KEY_TOKEN, token);
57+
result.putExtra(KEY_IS_SMS, getIntent().getBooleanExtra(KEY_IS_SMS, true));
58+
setResult(RESULT_OK, result);
59+
} else {
60+
setResult(RESULT_CANCELED);
61+
}
62+
63+
finish();
64+
}
65+
}

0 commit comments

Comments
 (0)