Support for multi-device provisioning flow.

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2015-01-19 20:24:10 -08:00
parent 48f6c2c526
commit 0c32001fe4
22 changed files with 2668 additions and 88 deletions

View File

@@ -0,0 +1,124 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.push.exceptions.NotFoundException;
import java.io.IOException;
public class DeviceProvisioningActivity extends PassphraseRequiredActionBarActivity {
private static final String TAG = DeviceProvisioningActivity.class.getSimpleName();
private Button continueButton;
private Button cancelButton;
private Uri uri;
private MasterSecret masterSecret;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.device_provisioning_activity);
initializeResources();
}
@Override
public void onNewMasterSecret(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
}
private void initializeResources() {
this.continueButton = (Button)findViewById(R.id.continue_button);
this.cancelButton = (Button)findViewById(R.id.cancel_button);
this.uri = getIntent().getData();
this.continueButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleProvisioning();
}
});
this.cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
private void handleProvisioning() {
new ProgressDialogAsyncTask<Void, Void, Integer>(this, "Adding device...", "Adding new device...") {
private static final int SUCCESS = 0;
private static final int NO_DEVICE = 1;
private static final int NETWORK_ERROR = 2;
private static final int KEY_ERROR = 3;
@Override
protected Integer doInBackground(Void... params) {
try {
Context context = DeviceProvisioningActivity.this;
TextSecureAccountManager accountManager = TextSecureCommunicationFactory.createManager(context);
String verificationCode = accountManager.getNewDeviceVerificationCode();
String ephemeralId = uri.getQueryParameter("uuid");
String publicKeyEncoded = uri.getQueryParameter("pub_key");
ECPublicKey publicKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
IdentityKeyPair identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, verificationCode);
return SUCCESS;
} catch (NotFoundException e) {
Log.w(TAG, e);
return NO_DEVICE;
} catch (IOException e) {
Log.w(TAG, e);
return NETWORK_ERROR;
} catch (InvalidKeyException e) {
Log.w(TAG, e);
return KEY_ERROR;
}
}
@Override
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
Context context = DeviceProvisioningActivity.this;
switch (result) {
case SUCCESS:
Toast.makeText(context, "Device added!", Toast.LENGTH_SHORT).show();
finish();
break;
case NO_DEVICE:
Toast.makeText(context, "No device found!", Toast.LENGTH_LONG).show();
break;
case NETWORK_ERROR:
Toast.makeText(context, "Network error!", Toast.LENGTH_LONG).show();
break;
case KEY_ERROR:
Toast.makeText(context, "Invalid QR code!", Toast.LENGTH_LONG).show();
break;
}
}
}.execute();
}
}

View File

@@ -16,11 +16,15 @@ import org.thoughtcrime.securesms.jobs.PushTextSendJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.push.SecurityEventListener;
import org.thoughtcrime.securesms.push.TextSecurePushTrustStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.TextSecureAccountManager;
import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
import org.whispersystems.textsecure.api.TextSecureMessageSender;
import org.whispersystems.textsecure.api.push.PushAddress;
import dagger.Module;
import dagger.Provides;
@@ -52,13 +56,21 @@ public class TextSecureCommunicationModule {
return new TextSecureMessageSenderFactory() {
@Override
public TextSecureMessageSender create(MasterSecret masterSecret) {
return new TextSecureMessageSender(Release.PUSH_URL,
new TextSecurePushTrustStore(context),
TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getPushServerPassword(context),
new TextSecureAxolotlStore(context, masterSecret),
Optional.of((TextSecureMessageSender.EventListener)
new SecurityEventListener(context)));
try {
String localNumber = TextSecurePreferences.getLocalNumber(context);
Recipient localRecipient = RecipientFactory.getRecipientsFromString(context, localNumber, false).getPrimaryRecipient();
return new TextSecureMessageSender(Release.PUSH_URL,
new TextSecurePushTrustStore(context),
TextSecurePreferences.getLocalNumber(context),
TextSecurePreferences.getPushServerPassword(context),
localRecipient.getRecipientId(),
new TextSecureAxolotlStore(context, masterSecret),
Optional.of((TextSecureMessageSender.EventListener)
new SecurityEventListener(context)));
} catch (RecipientFormattingException e) {
throw new AssertionError(e);
}
}
};
}

View File

@@ -46,7 +46,7 @@ public class DeliveryReceiptJob extends ContextJob implements InjectableType {
public void onRun() throws IOException {
Log.w("DeliveryReceiptJob", "Sending delivery receipt...");
TextSecureMessageSender messageSender = messageSenderFactory.create(null);
PushAddress pushAddress = new PushAddress(-1, destination, 1, relay);
PushAddress pushAddress = new PushAddress(-1, destination, relay);
messageSender.sendDeliveryReceipt(pushAddress, timestamp);
}

View File

@@ -5,11 +5,9 @@ import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.TextSecureDirectory;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.MmsMediaConstraints;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
@@ -20,7 +18,6 @@ import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.jobqueue.requirements.NetworkRequirement;
import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureAttachmentStream;
import org.thoughtcrime.securesms.database.TextSecureDirectory;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
@@ -82,7 +79,7 @@ public abstract class PushSendJob extends SendJob {
protected PushAddress getPushAddress(Recipient recipient) throws InvalidNumberException {
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
String relay = TextSecureDirectory.getInstance(context).getRelay(e164number);
return new PushAddress(recipient.getRecipientId(), e164number, 1, relay);
return new PushAddress(recipient.getRecipientId(), e164number, relay);
}
protected boolean isSmsFallbackApprovalRequired(String destination, boolean media) {

View File

@@ -16,8 +16,12 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
import org.thoughtcrime.securesms.push.TextSecureCommunicationFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -230,21 +234,27 @@ public class RegistrationService extends Service {
throws IOException
{
setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number));
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(this, masterSecret);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(this, masterSecret);
PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this, masterSecret);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(this, masterSecret, identityKey);
accountManager.setPreKeys(identityKey.getPublicKey(),lastResort, signedPreKey, records);
try {
Recipient self = RecipientFactory.getRecipientsFromString(this, number, false).getPrimaryRecipient();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(this, masterSecret);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(this, masterSecret);
PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this, masterSecret);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(this, masterSecret, identityKey);
accountManager.setPreKeys(identityKey.getPublicKey(),lastResort, signedPreKey, records);
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
setState(new RegistrationState(RegistrationState.STATE_GCM_REGISTERING, number));
String gcmRegistrationId = GoogleCloudMessaging.getInstance(this).register(GcmRefreshJob.REGISTRATION_ID);
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
accountManager.setGcmId(Optional.of(gcmRegistrationId));
String gcmRegistrationId = GoogleCloudMessaging.getInstance(this).register(GcmRefreshJob.REGISTRATION_ID);
TextSecurePreferences.setGcmRegistrationId(this, gcmRegistrationId);
accountManager.setGcmId(Optional.of(gcmRegistrationId));
DirectoryHelper.refreshDirectory(this, accountManager, number);
DatabaseFactory.getIdentityDatabase(this).saveIdentity(masterSecret, self.getRecipientId(), identityKey.getPublicKey());
DirectoryHelper.refreshDirectory(this, accountManager, number);
DirectoryRefreshListener.schedule(this);
DirectoryRefreshListener.schedule(this);
} catch (RecipientFormattingException e) {
throw new IOException(e);
}
}
private synchronized String waitForChallenge() throws AccountVerificationTimeoutException {