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.
This commit is contained in:
Moxie Marlinspike
2014-02-18 12:48:20 -08:00
parent abce678cb4
commit 3999171377
23 changed files with 435 additions and 100 deletions

View File

@@ -33,6 +33,7 @@ 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;
@@ -501,7 +502,8 @@ public class RegistrationProgressActivity extends SherlockActivity {
protected Integer doInBackground(Void... params) {
try {
PushServiceSocket socket = PushServiceSocketFactory.create(context, e164number, password);
socket.verifyAccount(code, signalingKey, true);
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
socket.verifyAccount(code, signalingKey, true, registrationId);
return SUCCESS;
} catch (RateLimitException e) {
Log.w("RegistrationProgressActivity", e);

View File

@@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.textsecure.crypto.IdentityKey;
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
import org.whispersystems.textsecure.crypto.InvalidKeyException;
@@ -106,6 +107,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
RatchetingSession.initializeSession(sessionRecord, ourBaseKey, theirBaseKey, ourEphemeralKey,
theirEphemeralKey, ourIdentityKey, theirIdentityKey);
Session.clearV1SessionFor(context, recipientDevice.getRecipient());
sessionRecord.setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
sessionRecord.setRemoteRegistrationId(message.getRegistrationId());
sessionRecord.save();
if (preKeyId != Medium.MAX_VALUE) {
@@ -134,6 +137,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
theirEphemeralKey, ourIdentityKey, theirIdentityKey);
sessionRecord.setPendingPreKey(message.getKeyId(), ourBaseKey.getPublicKey());
sessionRecord.setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
sessionRecord.setRemoteRegistrationId(message.getRegistrationId());
sessionRecord.save();
DatabaseFactory.getIdentityDatabase(context)

View File

@@ -227,6 +227,12 @@ public class RegistrationService extends Service {
String number = intent.getStringExtra("e164number");
MasterSecret masterSecret = intent.getParcelableExtra("master_secret");
int registrationId = TextSecurePreferences.getLocalRegistrationId(this);
if (registrationId == 0) {
registrationId = Util.generateRegistrationId();
TextSecurePreferences.setLocalRegistrationId(this, registrationId);
}
try {
String password = Util.getSecret(18);
@@ -236,13 +242,14 @@ public class RegistrationService extends Service {
initializeGcmRegistrationListener();
initializePreKeyGenerator(masterSecret);
setState(new RegistrationState(RegistrationState.STATE_CONNECTING, number));
PushServiceSocket socket = PushServiceSocketFactory.create(this, number, password);
socket.createAccount(false);
setState(new RegistrationState(RegistrationState.STATE_VERIFYING, number));
String challenge = waitForChallenge();
socket.verifyAccount(challenge, signalingKey, true);
socket.verifyAccount(challenge, signalingKey, true, registrationId);
handleCommonRegistration(masterSecret, socket, number);
markAsVerified(number, password, signalingKey);

View File

@@ -49,6 +49,8 @@ import org.whispersystems.textsecure.push.PushAttachmentPointer;
import org.whispersystems.textsecure.push.PushBody;
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
import org.whispersystems.textsecure.push.PushServiceSocket;
import org.whispersystems.textsecure.push.StaleDevices;
import org.whispersystems.textsecure.push.StaleDevicesException;
import org.whispersystems.textsecure.push.UnregisteredUserException;
import org.whispersystems.textsecure.storage.SessionRecordV2;
import org.whispersystems.textsecure.util.Base64;
@@ -146,6 +148,9 @@ public class PushTransport extends BaseTransport {
} catch (MismatchedDevicesException mde) {
Log.w("PushTransport", mde);
handleMismatchedDevices(socket, threadId, recipient, mde.getMismatchedDevices());
} catch (StaleDevicesException ste) {
Log.w("PushTransport", ste);
handleStaleDevices(recipient, ste.getStaleDevices());
}
}
}
@@ -211,6 +216,22 @@ public class PushTransport extends BaseTransport {
}
}
private void handleStaleDevices(Recipient recipient, StaleDevices staleDevices)
throws IOException
{
try {
long recipientId = recipient.getRecipientId();
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
for (int staleDeviceId : staleDevices.getStaleDevices()) {
PushAddress address = PushAddress.create(context, recipientId, e164number, staleDeviceId);
SessionRecordV2.delete(context, address);
}
} catch (InvalidNumberException e) {
throw new IOException(e);
}
}
private byte[] getPlaintextMessage(PushServiceSocket socket, SendReq message) throws IOException {
String messageBody = PartParser.getMessageText(message.getBody());
List<PushAttachmentPointer> attachments = getPushAttachmentPointers(socket, message.getBody());
@@ -321,11 +342,12 @@ public class PushTransport extends BaseTransport {
SessionCipher cipher = SessionCipher.createFor(context, masterSecret, pushAddress);
CiphertextMessage message = cipher.encrypt(plaintext);
int remoteRegistrationId = cipher.getRemoteRegistrationId();
if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, message.serialize());
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
} else if (message.getType() == CiphertextMessage.CURRENT_WHISPER_TYPE) {
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, message.serialize());
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
} else {
throw new AssertionError("Unknown ciphertext type: " + message.getType());
}

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.securesms.push.PushServiceSocketFactory;
import org.whispersystems.textsecure.directory.Directory;

View File

@@ -43,6 +43,16 @@ public class TextSecurePreferences {
private static final String DIRECTORY_FRESH_TIME_PREF = "pref_directory_refresh_time";
private static final String IN_THREAD_NOTIFICATION_PREF = "pref_key_inthread_notifications";
private static final String LOCAL_REGISTRATION_ID_PREF = "pref_local_registration_id";
public static int getLocalRegistrationId(Context context) {
return getIntegerPreference(context, LOCAL_REGISTRATION_ID_PREF, 0);
}
public static void setLocalRegistrationId(Context context, int registrationId) {
setIntegerPrefrence(context, LOCAL_REGISTRATION_ID_PREF, registrationId);
}
public static boolean isInThreadNotifications(Context context) {
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);
}