session-android/src/org/thoughtcrime/securesms/RegistrationProgressActivity.java
Moxie Marlinspike 3999171377 Introduce registration-time ID for detecting stale sessions.
1) At registration time, a client generates a random ID and
   transmits to the the server.

2) The server provides that registration ID to any client
   that requests a prekey.

3) Clients include that registration ID in any
   PreKeyWhisperMessage.

4) Clients include that registration ID in their sendMessage
   API call to the server.

5) The server verifies that the registration ID included in
   an API call is the same as the current registration ID
   for the destination device.  Otherwise, it notifies the
   sender that their session is stale.
2014-02-18 12:51:23 -08:00

609 lines
25 KiB
Java

package org.thoughtcrime.securesms;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockActivity;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.thoughtcrime.securesms.service.RegistrationService;
import org.thoughtcrime.securesms.util.ActionBarUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.MasterSecret;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.RateLimitException;
import org.whispersystems.textsecure.util.PhoneNumberFormatter;
import org.whispersystems.textsecure.util.Util;
import java.io.IOException;
import static org.thoughtcrime.securesms.service.RegistrationService.RegistrationState;
public class RegistrationProgressActivity extends SherlockActivity {
private static final int FOCUSED_COLOR = Color.parseColor("#ff333333");
private static final int UNFOCUSED_COLOR = Color.parseColor("#ff808080");
private ServiceConnection serviceConnection = new RegistrationServiceConnection();
private Handler registrationStateHandler = new RegistrationStateHandler();
private RegistrationReceiver registrationReceiver = new RegistrationReceiver();
private RegistrationService registrationService;
private LinearLayout registrationLayout;
private LinearLayout verificationFailureLayout;
private LinearLayout connectivityFailureLayout;
private RelativeLayout timeoutProgressLayout;
private ProgressBar registrationProgress;
private ProgressBar connectingProgress;
private ProgressBar verificationProgress;
private ProgressBar generatingKeysProgress;
private ProgressBar gcmRegistrationProgress;
private ImageView connectingCheck;
private ImageView verificationCheck;
private ImageView generatingKeysCheck;
private ImageView gcmRegistrationCheck;
private TextView connectingText;
private TextView verificationText;
private TextView registrationTimerText;
private TextView generatingKeysText;
private TextView gcmRegistrationText;
private Button verificationFailureButton;
private Button connectivityFailureButton;
private Button callButton;
private Button verifyButton;
private EditText codeEditText;
private MasterSecret masterSecret;
private volatile boolean visible;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
ActionBarUtil.initializeDefaultActionBar(this, getSupportActionBar(), getString(R.string.RegistrationProgressActivity_verifying_number));
setContentView(R.layout.registration_progress_activity);
initializeResources();
initializeLinks();
initializeServiceBinding();
}
@Override
public void onDestroy() {
super.onDestroy();
shutdownServiceBinding();
}
@Override
public void onResume() {
super.onResume();
handleActivityVisible();
}
@Override
public void onPause() {
super.onPause();
handleActivityNotVisible();
}
@Override
public void onBackPressed() {
}
private void initializeServiceBinding() {
Intent intent = new Intent(this, RegistrationService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeResources() {
this.masterSecret = getIntent().getParcelableExtra("master_secret");
this.registrationLayout = (LinearLayout)findViewById(R.id.registering_layout);
this.verificationFailureLayout = (LinearLayout)findViewById(R.id.verification_failure_layout);
this.connectivityFailureLayout = (LinearLayout)findViewById(R.id.connectivity_failure_layout);
this.registrationProgress = (ProgressBar) findViewById(R.id.registration_progress);
this.connectingProgress = (ProgressBar) findViewById(R.id.connecting_progress);
this.verificationProgress = (ProgressBar) findViewById(R.id.verification_progress);
this.generatingKeysProgress = (ProgressBar) findViewById(R.id.generating_keys_progress);
this.gcmRegistrationProgress = (ProgressBar) findViewById(R.id.gcm_registering_progress);
this.connectingCheck = (ImageView) findViewById(R.id.connecting_complete);
this.verificationCheck = (ImageView) findViewById(R.id.verification_complete);
this.generatingKeysCheck = (ImageView) findViewById(R.id.generating_keys_complete);
this.gcmRegistrationCheck = (ImageView) findViewById(R.id.gcm_registering_complete);
this.connectingText = (TextView) findViewById(R.id.connecting_text);
this.verificationText = (TextView) findViewById(R.id.verification_text);
this.registrationTimerText = (TextView) findViewById(R.id.registration_timer);
this.generatingKeysText = (TextView) findViewById(R.id.generating_keys_text);
this.gcmRegistrationText = (TextView) findViewById(R.id.gcm_registering_text);
this.verificationFailureButton = (Button) findViewById(R.id.verification_failure_edit_button);
this.connectivityFailureButton = (Button) findViewById(R.id.connectivity_failure_edit_button);
this.callButton = (Button) findViewById(R.id.call_button);
this.verifyButton = (Button) findViewById(R.id.verify_button);
this.codeEditText = (EditText) findViewById(R.id.telephone_code);
this.timeoutProgressLayout = (RelativeLayout) findViewById(R.id.timer_progress_layout);
Button editButton = (Button) findViewById(R.id.edit_button);
editButton.setOnClickListener(new EditButtonListener());
this.verificationFailureButton.setOnClickListener(new EditButtonListener());
this.connectivityFailureButton.setOnClickListener(new EditButtonListener());
}
private void initializeLinks() {
TextView failureText = (TextView) findViewById(R.id.sms_failed_text);
String pretext = getString(R.string.registration_progress_activity__textsecure_timed_out_while_waiting_for_a_verification_sms_message);
String link = getString(R.string.RegistrationProblemsActivity_possible_problems);
SpannableString spannableString = new SpannableString(pretext + " " + link);
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
Intent intent = new Intent(RegistrationProgressActivity.this,
RegistrationProblemsActivity.class);
startActivity(intent);
}
}, pretext.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
failureText.setText(spannableString);
failureText.setMovementMethod(LinkMovementMethod.getInstance());
}
private void handleActivityVisible() {
IntentFilter filter = new IntentFilter(RegistrationService.REGISTRATION_EVENT);
filter.setPriority(1000);
registerReceiver(registrationReceiver, filter);
visible = true;
}
private void handleActivityNotVisible() {
unregisterReceiver(registrationReceiver);
visible = false;
}
private void handleStateIdle() {
if (hasNumberDirective()) {
Intent intent = new Intent(this, RegistrationService.class);
intent.setAction(RegistrationService.REGISTER_NUMBER_ACTION);
intent.putExtra("e164number", getNumberDirective());
intent.putExtra("master_secret", masterSecret);
startService(intent);
} else {
Intent intent = new Intent(this, RegistrationActivity.class);
intent.putExtra("master_secret", masterSecret);
startActivity(intent);
finish();
}
}
private void handleStateConnecting() {
this.registrationLayout.setVisibility(View.VISIBLE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.connectingProgress.setVisibility(View.VISIBLE);
this.connectingCheck.setVisibility(View.INVISIBLE);
this.verificationProgress.setVisibility(View.INVISIBLE);
this.verificationCheck.setVisibility(View.INVISIBLE);
this.generatingKeysProgress.setVisibility(View.INVISIBLE);
this.generatingKeysCheck.setVisibility(View.INVISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(FOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.timeoutProgressLayout.setVisibility(View.VISIBLE);
}
private void handleStateVerifying() {
this.registrationLayout.setVisibility(View.VISIBLE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.connectingProgress.setVisibility(View.INVISIBLE);
this.connectingCheck.setVisibility(View.VISIBLE);
this.verificationProgress.setVisibility(View.VISIBLE);
this.verificationCheck.setVisibility(View.INVISIBLE);
this.generatingKeysProgress.setVisibility(View.INVISIBLE);
this.generatingKeysCheck.setVisibility(View.INVISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(FOCUSED_COLOR);
this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.registrationProgress.setVisibility(View.VISIBLE);
this.timeoutProgressLayout.setVisibility(View.VISIBLE);
}
private void handleStateGeneratingKeys() {
this.registrationLayout.setVisibility(View.VISIBLE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.connectingProgress.setVisibility(View.INVISIBLE);
this.connectingCheck.setVisibility(View.VISIBLE);
this.verificationProgress.setVisibility(View.INVISIBLE);
this.verificationCheck.setVisibility(View.VISIBLE);
this.generatingKeysProgress.setVisibility(View.VISIBLE);
this.generatingKeysCheck.setVisibility(View.INVISIBLE);
this.gcmRegistrationProgress.setVisibility(View.INVISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.generatingKeysText.setTextColor(FOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(UNFOCUSED_COLOR);
this.registrationProgress.setVisibility(View.INVISIBLE);
this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
}
private void handleStateGcmRegistering() {
this.registrationLayout.setVisibility(View.VISIBLE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.connectingProgress.setVisibility(View.INVISIBLE);
this.connectingCheck.setVisibility(View.VISIBLE);
this.verificationProgress.setVisibility(View.INVISIBLE);
this.verificationCheck.setVisibility(View.VISIBLE);
this.generatingKeysProgress.setVisibility(View.INVISIBLE);
this.generatingKeysCheck.setVisibility(View.VISIBLE);
this.gcmRegistrationProgress.setVisibility(View.VISIBLE);
this.gcmRegistrationCheck.setVisibility(View.INVISIBLE);
this.connectingText.setTextColor(UNFOCUSED_COLOR);
this.verificationText.setTextColor(UNFOCUSED_COLOR);
this.generatingKeysText.setTextColor(UNFOCUSED_COLOR);
this.gcmRegistrationText.setTextColor(FOCUSED_COLOR);
this.registrationProgress.setVisibility(View.INVISIBLE);
this.timeoutProgressLayout.setVisibility(View.INVISIBLE);
}
private void handleGcmTimeout(RegistrationState state) {
handleConnectivityError(state);
}
private void handleVerificationRequestedVoice(RegistrationState state) {
handleVerificationTimeout(state);
verifyButton.setOnClickListener(new VerifyClickListener(state.number, state.password));
verifyButton.setEnabled(true);
codeEditText.setEnabled(true);
}
private void handleVerificationTimeout(RegistrationState state) {
this.callButton.setOnClickListener(new CallClickListener(state.number));
this.verifyButton.setEnabled(false);
this.codeEditText.setEnabled(false);
this.registrationLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.GONE);
this.verificationFailureLayout.setVisibility(View.VISIBLE);
this.verificationFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
PhoneNumberFormatter.formatNumberInternational(state.number)));
}
private void handleConnectivityError(RegistrationState state) {
this.registrationLayout.setVisibility(View.GONE);
this.verificationFailureLayout.setVisibility(View.GONE);
this.connectivityFailureLayout.setVisibility(View.VISIBLE);
this.connectivityFailureButton.setText(String.format(getString(R.string.RegistrationProgressActivity_edit_s),
PhoneNumberFormatter.formatNumberInternational(state.number)));
}
private void handleVerificationComplete() {
if (visible) {
Toast.makeText(this,
R.string.RegistrationProgressActivity_registration_complete,
Toast.LENGTH_LONG).show();
}
shutdownService();
startActivity(new Intent(this, RoutingActivity.class));
finish();
}
private void handleTimerUpdate() {
if (registrationService == null)
return;
int totalSecondsRemaining = registrationService.getSecondsRemaining();
int minutesRemaining = totalSecondsRemaining / 60;
int secondsRemaining = totalSecondsRemaining - (minutesRemaining * 60);
double percentageComplete = (double)((60 * 2) - totalSecondsRemaining) / (double)(60 * 2);
int progress = (int)Math.round(((double)registrationProgress.getMax()) * percentageComplete);
this.registrationProgress.setProgress(progress);
this.registrationTimerText.setText(String.format("%02d:%02d", minutesRemaining, secondsRemaining));
registrationStateHandler.sendEmptyMessageDelayed(RegistrationState.STATE_TIMER, 1000);
}
private boolean hasNumberDirective() {
return getIntent().getStringExtra("e164number") != null;
}
private String getNumberDirective() {
return getIntent().getStringExtra("e164number");
}
private void shutdownServiceBinding() {
if (serviceConnection != null) {
unbindService(serviceConnection);
serviceConnection = null;
}
}
private void shutdownService() {
if (registrationService != null) {
registrationService.shutdown();
registrationService = null;
}
shutdownServiceBinding();
Intent serviceIntent = new Intent(RegistrationProgressActivity.this, RegistrationService.class);
stopService(serviceIntent);
}
private class RegistrationServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
registrationService = ((RegistrationService.RegistrationServiceBinder)service).getService();
registrationService.setRegistrationStateHandler(registrationStateHandler);
RegistrationState state = registrationService.getRegistrationState();
registrationStateHandler.obtainMessage(state.state, state).sendToTarget();
handleTimerUpdate();
}
@Override
public void onServiceDisconnected(ComponentName name) {
registrationService.setRegistrationStateHandler(null);
}
}
private class RegistrationStateHandler extends Handler {
@Override
public void handleMessage(Message message) {
RegistrationState state = (RegistrationState)message.obj;
switch (message.what) {
case RegistrationState.STATE_IDLE: handleStateIdle(); break;
case RegistrationState.STATE_CONNECTING: handleStateConnecting(); break;
case RegistrationState.STATE_VERIFYING: handleStateVerifying(); break;
case RegistrationState.STATE_TIMER: handleTimerUpdate(); break;
case RegistrationState.STATE_GENERATING_KEYS: handleStateGeneratingKeys(); break;
case RegistrationState.STATE_GCM_REGISTERING: handleStateGcmRegistering(); break;
case RegistrationState.STATE_TIMEOUT: handleVerificationTimeout(state); break;
case RegistrationState.STATE_COMPLETE: handleVerificationComplete(); break;
case RegistrationState.STATE_GCM_TIMEOUT: handleGcmTimeout(state); break;
case RegistrationState.STATE_NETWORK_ERROR: handleConnectivityError(state); break;
case RegistrationState.STATE_VOICE_REQUESTED: handleVerificationRequestedVoice(state); break;
}
}
}
private class EditButtonListener implements View.OnClickListener {
@Override
public void onClick(View v) {
shutdownService();
Intent activityIntent = new Intent(RegistrationProgressActivity.this, RegistrationActivity.class);
activityIntent.putExtra("master_secret", masterSecret);
startActivity(activityIntent);
finish();
}
}
private class RegistrationReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
abortBroadcast();
}
}
private class VerifyClickListener implements View.OnClickListener {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;
private static final int RATE_LIMIT_ERROR = 2;
private static final int VERIFICATION_ERROR = 3;
private final String e164number;
private final String password;
private final String signalingKey;
private final Context context;
private ProgressDialog progressDialog;
public VerifyClickListener(String e164number, String password) {
this.e164number = e164number;
this.password = password;
this.signalingKey = Util.getSecret(52);
this.context = RegistrationProgressActivity.this;
}
@Override
public void onClick(View v) {
final String code = codeEditText.getText().toString();
if (Util.isEmpty(code)) {
Toast.makeText(context,
getString(R.string.RegistrationProgressActivity_you_must_enter_the_code_you_received_first),
Toast.LENGTH_LONG).show();
return;
}
new AsyncTask<Void, Void, Integer>() {
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(context,
getString(R.string.RegistrationProgressActivity_connecting),
getString(R.string.RegistrationProgressActivity_connecting_for_verification),
true, false);
}
@Override
protected void onPostExecute(Integer result) {
if (progressDialog != null) progressDialog.dismiss();
switch (result) {
case SUCCESS:
Intent intent = new Intent(context, RegistrationService.class);
intent.setAction(RegistrationService.VOICE_REGISTER_ACTION);
intent.putExtra("e164number", e164number);
intent.putExtra("password", password);
intent.putExtra("signaling_key", signalingKey);
intent.putExtra("master_secret", masterSecret);
startService(intent);
break;
case NETWORK_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_network_error),
getString(R.string.RegistrationProgressActivity_unable_to_connect));
break;
case VERIFICATION_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_verification_failed),
getString(R.string.RegistrationProgressActivity_the_verification_code_you_submitted_is_incorrect));
break;
case RATE_LIMIT_ERROR:
Util.showAlertDialog(context, getString(R.string.RegistrationProgressActivity_too_many_attempts),
getString(R.string.RegistrationProgressActivity_youve_submitted_an_incorrect_verification_code_too_many_times));
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context, e164number, password);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
socket.verifyAccount(code, signalingKey, true, registrationId);
return SUCCESS;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_ERROR;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
}
}
}.execute();
}
}
private class CallClickListener implements View.OnClickListener {
private static final int SUCCESS = 0;
private static final int NETWORK_ERROR = 1;
private static final int RATE_LIMIT_EXCEEDED = 2;
private static final int CREATE_ERROR = 3;
private final String e164number;
private final String password;
private final Context context;
public CallClickListener(String e164number) {
this.e164number = e164number;
this.password = Util.getSecret(18);
this.context = RegistrationProgressActivity.this;
}
@Override
public void onClick(View v) {
new AsyncTask<Void, Void, Integer>() {
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = ProgressDialog.show(context,
getString(R.string.RegistrationProgressActivity_requesting_call),
getString(R.string.RegistrationProgressActivity_requesting_incoming_call),
true, false);
}
@Override
protected void onPostExecute(Integer result) {
if (progressDialog != null) progressDialog.dismiss();
switch (result) {
case SUCCESS:
Intent intent = new Intent(context, RegistrationService.class);
intent.setAction(RegistrationService.VOICE_REQUESTED_ACTION);
intent.putExtra("e164number", e164number);
intent.putExtra("password", password);
intent.putExtra("master_secret", masterSecret);
startService(intent);
callButton.setEnabled(false);
new Handler().postDelayed(new Runnable(){
@Override
public void run() {
callButton.setEnabled(true);
}
}, 15000);
break;
case NETWORK_ERROR:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_network_error),
getString(R.string.RegistrationProgressActivity_unable_to_connect));
break;
case CREATE_ERROR:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_server_error),
getString(R.string.RegistrationProgressActivity_the_server_encountered_an_error));
break;
case RATE_LIMIT_EXCEEDED:
Util.showAlertDialog(context,
getString(R.string.RegistrationProgressActivity_too_many_requests),
getString(R.string.RegistrationProgressActivity_youve_already_requested_a_voice_call));
break;
}
}
@Override
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context, e164number, password);
socket.createAccount(true);
return SUCCESS;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);
return RATE_LIMIT_EXCEEDED;
} catch (IOException e) {
Log.w("RegistrationProgressActivity", e);
return NETWORK_ERROR;
}
}
}.execute();
}
}
}