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

@ -51,6 +51,9 @@ message SessionStructure {
optional PendingKeyExchange pendingKeyExchange = 8;
optional PendingPreKey pendingPreKey = 9;
optional uint32 remoteRegistrationId = 10;
optional uint32 localRegistrationId = 11;
}
message PreKeyRecordStructure {

View file

@ -11,6 +11,7 @@ message WhisperMessage {
}
message PreKeyWhisperMessage {
optional uint32 registrationId = 5;
optional uint32 preKeyId = 1;
optional bytes baseKey = 2;
optional bytes identityKey = 3;

View file

@ -30,6 +30,7 @@ public abstract class SessionCipher {
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
public abstract int getRemoteRegistrationId();
public static SessionCipher createFor(Context context,
MasterSecret masterSecret,

View file

@ -82,6 +82,11 @@ public class SessionCipherV1 extends SessionCipher {
}
}
@Override
public int getRemoteRegistrationId() {
return 0;
}
private SessionCipherContext getEncryptionContext() {
try {
KeyRecords records = getKeyRecords(context, masterSecret, recipient);

View file

@ -36,9 +36,9 @@ public class SessionCipherV2 extends SessionCipher {
MasterSecret masterSecret,
RecipientDevice recipient)
{
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
this.context = context;
this.masterSecret = masterSecret;
this.recipient = recipient;
}
@Override
@ -56,8 +56,11 @@ public class SessionCipherV2 extends SessionCipher {
previousCounter, ciphertextBody);
if (sessionRecord.hasPendingPreKey()) {
Pair<Integer, ECPublicKey> pendingPreKey = sessionRecord.getPendingPreKey();
ciphertextMessage = new PreKeyWhisperMessage(pendingPreKey.first, pendingPreKey.second,
Pair<Integer, ECPublicKey> pendingPreKey = sessionRecord.getPendingPreKey();
int localRegistrationId = sessionRecord.getLocalRegistrationId();
ciphertextMessage = new PreKeyWhisperMessage(localRegistrationId, pendingPreKey.first,
pendingPreKey.second,
sessionRecord.getLocalIdentityKey(),
(WhisperMessageV2) ciphertextMessage);
}
@ -91,6 +94,14 @@ public class SessionCipherV2 extends SessionCipher {
}
}
@Override
public int getRemoteRegistrationId() {
synchronized (SESSION_LOCK) {
SessionRecordV2 sessionRecord = getSessionRecord();
return sessionRecord.getRemoteRegistrationId();
}
}
private ChainKey getOrCreateChainKey(SessionRecordV2 sessionRecord, ECPublicKey theirEphemeral)
throws InvalidMessageException
{

View file

@ -15,6 +15,7 @@ import org.whispersystems.textsecure.util.Util;
public class PreKeyWhisperMessage implements CiphertextMessage {
private final int version;
private final int registrationId;
private final int preKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
@ -43,11 +44,12 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
throw new InvalidMessageException("Incomplete message.");
}
this.serialized = serialized;
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
this.serialized = serialized;
this.registrationId = preKeyWhisperMessage.getRegistrationId();
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
} catch (InvalidProtocolBufferException e) {
throw new InvalidMessageException(e);
} catch (InvalidKeyException e) {
@ -55,14 +57,15 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
}
}
public PreKeyWhisperMessage(int preKeyId, ECPublicKey baseKey, IdentityKey identityKey,
WhisperMessageV2 message)
public PreKeyWhisperMessage(int registrationId, int preKeyId, ECPublicKey baseKey,
IdentityKey identityKey, WhisperMessageV2 message)
{
this.version = CiphertextMessage.CURRENT_VERSION;
this.preKeyId = preKeyId;
this.baseKey = baseKey;
this.identityKey = identityKey;
this.message = message;
this.version = CiphertextMessage.CURRENT_VERSION;
this.registrationId = registrationId;
this.preKeyId = preKeyId;
this.baseKey = baseKey;
this.identityKey = identityKey;
this.message = message;
byte[] versionBytes = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, this.version)};
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
@ -70,6 +73,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setMessage(ByteString.copyFrom(message.serialize()))
.setRegistrationId(registrationId)
.build().toByteArray();
this.serialized = Util.combine(versionBytes, messageBytes);
@ -79,6 +83,10 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
return identityKey;
}
public int getRegistrationId() {
return registrationId;
}
public int getPreKeyId() {
return preKeyId;
}

View file

@ -526,6 +526,10 @@ public final class WhisperProtos {
public interface PreKeyWhisperMessageOrBuilder
extends com.google.protobuf.MessageOrBuilder {
// optional uint32 registrationId = 5;
boolean hasRegistrationId();
int getRegistrationId();
// optional uint32 preKeyId = 1;
boolean hasPreKeyId();
int getPreKeyId();
@ -571,11 +575,21 @@ public final class WhisperProtos {
}
private int bitField0_;
// optional uint32 registrationId = 5;
public static final int REGISTRATIONID_FIELD_NUMBER = 5;
private int registrationId_;
public boolean hasRegistrationId() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
public int getRegistrationId() {
return registrationId_;
}
// optional uint32 preKeyId = 1;
public static final int PREKEYID_FIELD_NUMBER = 1;
private int preKeyId_;
public boolean hasPreKeyId() {
return ((bitField0_ & 0x00000001) == 0x00000001);
return ((bitField0_ & 0x00000002) == 0x00000002);
}
public int getPreKeyId() {
return preKeyId_;
@ -585,7 +599,7 @@ public final class WhisperProtos {
public static final int BASEKEY_FIELD_NUMBER = 2;
private com.google.protobuf.ByteString baseKey_;
public boolean hasBaseKey() {
return ((bitField0_ & 0x00000002) == 0x00000002);
return ((bitField0_ & 0x00000004) == 0x00000004);
}
public com.google.protobuf.ByteString getBaseKey() {
return baseKey_;
@ -595,7 +609,7 @@ public final class WhisperProtos {
public static final int IDENTITYKEY_FIELD_NUMBER = 3;
private com.google.protobuf.ByteString identityKey_;
public boolean hasIdentityKey() {
return ((bitField0_ & 0x00000004) == 0x00000004);
return ((bitField0_ & 0x00000008) == 0x00000008);
}
public com.google.protobuf.ByteString getIdentityKey() {
return identityKey_;
@ -605,13 +619,14 @@ public final class WhisperProtos {
public static final int MESSAGE_FIELD_NUMBER = 4;
private com.google.protobuf.ByteString message_;
public boolean hasMessage() {
return ((bitField0_ & 0x00000008) == 0x00000008);
return ((bitField0_ & 0x00000010) == 0x00000010);
}
public com.google.protobuf.ByteString getMessage() {
return message_;
}
private void initFields() {
registrationId_ = 0;
preKeyId_ = 0;
baseKey_ = com.google.protobuf.ByteString.EMPTY;
identityKey_ = com.google.protobuf.ByteString.EMPTY;
@ -629,18 +644,21 @@ public final class WhisperProtos {
public void writeTo(com.google.protobuf.CodedOutputStream output)
throws java.io.IOException {
getSerializedSize();
if (((bitField0_ & 0x00000001) == 0x00000001)) {
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeUInt32(1, preKeyId_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
if (((bitField0_ & 0x00000004) == 0x00000004)) {
output.writeBytes(2, baseKey_);
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
if (((bitField0_ & 0x00000008) == 0x00000008)) {
output.writeBytes(3, identityKey_);
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
if (((bitField0_ & 0x00000010) == 0x00000010)) {
output.writeBytes(4, message_);
}
if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeUInt32(5, registrationId_);
}
getUnknownFields().writeTo(output);
}
@ -650,22 +668,26 @@ public final class WhisperProtos {
if (size != -1) return size;
size = 0;
if (((bitField0_ & 0x00000001) == 0x00000001)) {
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(1, preKeyId_);
}
if (((bitField0_ & 0x00000002) == 0x00000002)) {
if (((bitField0_ & 0x00000004) == 0x00000004)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, baseKey_);
}
if (((bitField0_ & 0x00000004) == 0x00000004)) {
if (((bitField0_ & 0x00000008) == 0x00000008)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(3, identityKey_);
}
if (((bitField0_ & 0x00000008) == 0x00000008)) {
if (((bitField0_ & 0x00000010) == 0x00000010)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(4, message_);
}
if (((bitField0_ & 0x00000001) == 0x00000001)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(5, registrationId_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@ -790,14 +812,16 @@ public final class WhisperProtos {
public Builder clear() {
super.clear();
preKeyId_ = 0;
registrationId_ = 0;
bitField0_ = (bitField0_ & ~0x00000001);
baseKey_ = com.google.protobuf.ByteString.EMPTY;
preKeyId_ = 0;
bitField0_ = (bitField0_ & ~0x00000002);
identityKey_ = com.google.protobuf.ByteString.EMPTY;
baseKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000004);
message_ = com.google.protobuf.ByteString.EMPTY;
identityKey_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000008);
message_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000010);
return this;
}
@ -839,18 +863,22 @@ public final class WhisperProtos {
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
to_bitField0_ |= 0x00000001;
}
result.preKeyId_ = preKeyId_;
result.registrationId_ = registrationId_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.baseKey_ = baseKey_;
result.preKeyId_ = preKeyId_;
if (((from_bitField0_ & 0x00000004) == 0x00000004)) {
to_bitField0_ |= 0x00000004;
}
result.identityKey_ = identityKey_;
result.baseKey_ = baseKey_;
if (((from_bitField0_ & 0x00000008) == 0x00000008)) {
to_bitField0_ |= 0x00000008;
}
result.identityKey_ = identityKey_;
if (((from_bitField0_ & 0x00000010) == 0x00000010)) {
to_bitField0_ |= 0x00000010;
}
result.message_ = message_;
result.bitField0_ = to_bitField0_;
onBuilt();
@ -868,6 +896,9 @@ public final class WhisperProtos {
public Builder mergeFrom(org.whispersystems.textsecure.crypto.protocol.WhisperProtos.PreKeyWhisperMessage other) {
if (other == org.whispersystems.textsecure.crypto.protocol.WhisperProtos.PreKeyWhisperMessage.getDefaultInstance()) return this;
if (other.hasRegistrationId()) {
setRegistrationId(other.getRegistrationId());
}
if (other.hasPreKeyId()) {
setPreKeyId(other.getPreKeyId());
}
@ -912,47 +943,73 @@ public final class WhisperProtos {
break;
}
case 8: {
bitField0_ |= 0x00000001;
bitField0_ |= 0x00000002;
preKeyId_ = input.readUInt32();
break;
}
case 18: {
bitField0_ |= 0x00000002;
bitField0_ |= 0x00000004;
baseKey_ = input.readBytes();
break;
}
case 26: {
bitField0_ |= 0x00000004;
bitField0_ |= 0x00000008;
identityKey_ = input.readBytes();
break;
}
case 34: {
bitField0_ |= 0x00000008;
bitField0_ |= 0x00000010;
message_ = input.readBytes();
break;
}
case 40: {
bitField0_ |= 0x00000001;
registrationId_ = input.readUInt32();
break;
}
}
}
}
private int bitField0_;
// optional uint32 registrationId = 5;
private int registrationId_ ;
public boolean hasRegistrationId() {
return ((bitField0_ & 0x00000001) == 0x00000001);
}
public int getRegistrationId() {
return registrationId_;
}
public Builder setRegistrationId(int value) {
bitField0_ |= 0x00000001;
registrationId_ = value;
onChanged();
return this;
}
public Builder clearRegistrationId() {
bitField0_ = (bitField0_ & ~0x00000001);
registrationId_ = 0;
onChanged();
return this;
}
// optional uint32 preKeyId = 1;
private int preKeyId_ ;
public boolean hasPreKeyId() {
return ((bitField0_ & 0x00000001) == 0x00000001);
return ((bitField0_ & 0x00000002) == 0x00000002);
}
public int getPreKeyId() {
return preKeyId_;
}
public Builder setPreKeyId(int value) {
bitField0_ |= 0x00000001;
bitField0_ |= 0x00000002;
preKeyId_ = value;
onChanged();
return this;
}
public Builder clearPreKeyId() {
bitField0_ = (bitField0_ & ~0x00000001);
bitField0_ = (bitField0_ & ~0x00000002);
preKeyId_ = 0;
onChanged();
return this;
@ -961,7 +1018,7 @@ public final class WhisperProtos {
// optional bytes baseKey = 2;
private com.google.protobuf.ByteString baseKey_ = com.google.protobuf.ByteString.EMPTY;
public boolean hasBaseKey() {
return ((bitField0_ & 0x00000002) == 0x00000002);
return ((bitField0_ & 0x00000004) == 0x00000004);
}
public com.google.protobuf.ByteString getBaseKey() {
return baseKey_;
@ -970,13 +1027,13 @@ public final class WhisperProtos {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
bitField0_ |= 0x00000004;
baseKey_ = value;
onChanged();
return this;
}
public Builder clearBaseKey() {
bitField0_ = (bitField0_ & ~0x00000002);
bitField0_ = (bitField0_ & ~0x00000004);
baseKey_ = getDefaultInstance().getBaseKey();
onChanged();
return this;
@ -985,7 +1042,7 @@ public final class WhisperProtos {
// optional bytes identityKey = 3;
private com.google.protobuf.ByteString identityKey_ = com.google.protobuf.ByteString.EMPTY;
public boolean hasIdentityKey() {
return ((bitField0_ & 0x00000004) == 0x00000004);
return ((bitField0_ & 0x00000008) == 0x00000008);
}
public com.google.protobuf.ByteString getIdentityKey() {
return identityKey_;
@ -994,13 +1051,13 @@ public final class WhisperProtos {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000004;
bitField0_ |= 0x00000008;
identityKey_ = value;
onChanged();
return this;
}
public Builder clearIdentityKey() {
bitField0_ = (bitField0_ & ~0x00000004);
bitField0_ = (bitField0_ & ~0x00000008);
identityKey_ = getDefaultInstance().getIdentityKey();
onChanged();
return this;
@ -1009,7 +1066,7 @@ public final class WhisperProtos {
// optional bytes message = 4;
private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY;
public boolean hasMessage() {
return ((bitField0_ & 0x00000008) == 0x00000008);
return ((bitField0_ & 0x00000010) == 0x00000010);
}
public com.google.protobuf.ByteString getMessage() {
return message_;
@ -1018,13 +1075,13 @@ public final class WhisperProtos {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000008;
bitField0_ |= 0x00000010;
message_ = value;
onChanged();
return this;
}
public Builder clearMessage() {
bitField0_ = (bitField0_ & ~0x00000008);
bitField0_ = (bitField0_ & ~0x00000010);
message_ = getDefaultInstance().getMessage();
onChanged();
return this;
@ -1586,13 +1643,14 @@ public final class WhisperProtos {
"\n\031WhisperTextProtocol.proto\022\ntextsecure\"" +
"d\n\016WhisperMessage\022\024\n\014ephemeralKey\030\001 \001(\014\022" +
"\017\n\007counter\030\002 \001(\r\022\027\n\017previousCounter\030\003 \001(" +
"\r\022\022\n\nciphertext\030\004 \001(\014\"_\n\024PreKeyWhisperMe" +
"ssage\022\020\n\010preKeyId\030\001 \001(\r\022\017\n\007baseKey\030\002 \001(\014" +
"\022\023\n\013identityKey\030\003 \001(\014\022\017\n\007message\030\004 \001(\014\"\\" +
"\n\022KeyExchangeMessage\022\n\n\002id\030\001 \001(\r\022\017\n\007base" +
"Key\030\002 \001(\014\022\024\n\014ephemeralKey\030\003 \001(\014\022\023\n\013ident" +
"ityKey\030\004 \001(\014B>\n-org.whispersystems.texts" +
"ecure.crypto.protocolB\rWhisperProtos"
"\r\022\022\n\nciphertext\030\004 \001(\014\"w\n\024PreKeyWhisperMe" +
"ssage\022\026\n\016registrationId\030\005 \001(\r\022\020\n\010preKeyI" +
"d\030\001 \001(\r\022\017\n\007baseKey\030\002 \001(\014\022\023\n\013identityKey\030" +
"\003 \001(\014\022\017\n\007message\030\004 \001(\014\"\\\n\022KeyExchangeMes" +
"sage\022\n\n\002id\030\001 \001(\r\022\017\n\007baseKey\030\002 \001(\014\022\024\n\014eph" +
"emeralKey\030\003 \001(\014\022\023\n\013identityKey\030\004 \001(\014B>\n-" +
"org.whispersystems.textsecure.crypto.pro",
"tocolB\rWhisperProtos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -1612,7 +1670,7 @@ public final class WhisperProtos {
internal_static_textsecure_PreKeyWhisperMessage_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_PreKeyWhisperMessage_descriptor,
new java.lang.String[] { "PreKeyId", "BaseKey", "IdentityKey", "Message", },
new java.lang.String[] { "RegistrationId", "PreKeyId", "BaseKey", "IdentityKey", "Message", },
org.whispersystems.textsecure.crypto.protocol.WhisperProtos.PreKeyWhisperMessage.class,
org.whispersystems.textsecure.crypto.protocol.WhisperProtos.PreKeyWhisperMessage.Builder.class);
internal_static_textsecure_KeyExchangeMessage_descriptor =

View file

@ -2,12 +2,14 @@ package org.whispersystems.textsecure.push;
public class AccountAttributes {
private String signalingKey;
private String signalingKey;
private boolean supportsSms;
private int registrationId;
public AccountAttributes(String signalingKey, boolean supportsSms) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) {
this.signalingKey = signalingKey;
this.supportsSms = supportsSms;
this.registrationId = registrationId;
}
public AccountAttributes() {}
@ -19,4 +21,8 @@ public class AccountAttributes {
public boolean isSupportsSms() {
return supportsSms;
}
public int getRegistrationId() {
return registrationId;
}
}

View file

@ -22,12 +22,14 @@ public class OutgoingPushMessage {
private int type;
private int destinationDeviceId;
private int destinationRegistrationId;
private String body;
public OutgoingPushMessage(PushAddress address, PushBody body) {
this.type = body.getType();
this.destinationDeviceId = address.getDeviceId();
this.body = Base64.encodeBytes(body.getBody());
this.type = body.getType();
this.destinationDeviceId = address.getDeviceId();
this.destinationRegistrationId = body.getRemoteRegistrationId();
this.body = Base64.encodeBytes(body.getBody());
}
public int getDestinationDeviceId() {
@ -41,4 +43,8 @@ public class OutgoingPushMessage {
public int getType() {
return type;
}
public int getDestinationRegistrationId() {
return destinationRegistrationId;
}
}

View file

@ -27,11 +27,13 @@ public class PreKeyEntity {
private int keyId;
private ECPublicKey publicKey;
private IdentityKey identityKey;
private int registrationId;
public PreKeyEntity(int keyId, ECPublicKey publicKey, IdentityKey identityKey) {
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.keyId = keyId;
this.publicKey = publicKey;
this.identityKey = identityKey;
this.registrationId = registrationId;
}
public int getDeviceId() {
@ -50,6 +52,10 @@ public class PreKeyEntity {
return identityKey;
}
public int getRegistrationId() {
return registrationId;
}
public static String toJson(PreKeyEntity entity) {
return getBuilder().create().toJson(entity);
}
@ -66,6 +72,7 @@ public class PreKeyEntity {
return builder;
}
private static class ECPublicKeyJsonAdapter
implements JsonSerializer<ECPublicKey>, JsonDeserializer<ECPublicKey>
{

View file

@ -3,11 +3,13 @@ package org.whispersystems.textsecure.push;
public class PushBody {
private final int type;
private final int remoteRegistrationId;
private final byte[] body;
public PushBody(int type, byte[] body) {
this.type = type;
this.body = body;
public PushBody(int type, int remoteRegistrationId, byte[] body) {
this.type = type;
this.remoteRegistrationId = remoteRegistrationId;
this.body = body;
}
public int getType() {
@ -17,4 +19,8 @@ public class PushBody {
public byte[] getBody() {
return body;
}
public int getRemoteRegistrationId() {
return remoteRegistrationId;
}
}

View file

@ -71,10 +71,11 @@ public class PushServiceSocket {
makeRequest(String.format(path, localNumber), "GET", null);
}
public void verifyAccount(String verificationCode, String signalingKey, boolean supportsSms)
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int registrationId)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms);
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
"PUT", new Gson().toJson(signalingKeyEntity));
}
@ -324,6 +325,11 @@ public class PushServiceSocket {
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
}
if (connection.getResponseCode() == 410) {
String response = Util.readFully(connection.getErrorStream());
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
}
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}

View file

@ -0,0 +1,12 @@
package org.whispersystems.textsecure.push;
import java.util.List;
public class StaleDevices {
private List<Integer> staleDevices;
public List<Integer> getStaleDevices() {
return staleDevices;
}
}

View file

@ -0,0 +1,16 @@
package org.whispersystems.textsecure.push;
import java.io.IOException;
public class StaleDevicesException extends IOException {
private final StaleDevices staleDevices;
public StaleDevicesException(StaleDevices staleDevices) {
this.staleDevices = staleDevices;
}
public StaleDevices getStaleDevices() {
return staleDevices;
}
}

View file

@ -497,6 +497,26 @@ public class SessionRecordV2 extends Record {
.build();
}
public void setRemoteRegistrationId(int registrationId) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setRemoteRegistrationId(registrationId)
.build();
}
public int getRemoteRegistrationId() {
return this.sessionStructure.getRemoteRegistrationId();
}
public void setLocalRegistrationId(int registrationId) {
this.sessionStructure = this.sessionStructure.toBuilder()
.setLocalRegistrationId(registrationId)
.build();
}
public int getLocalRegistrationId() {
return this.sessionStructure.getLocalRegistrationId();
}
public void save() {
synchronized (FILE_LOCK) {
try {

View file

@ -55,6 +55,14 @@ public final class StorageProtos {
boolean hasPendingPreKey();
org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey getPendingPreKey();
org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKeyOrBuilder getPendingPreKeyOrBuilder();
// optional uint32 remoteRegistrationId = 10;
boolean hasRemoteRegistrationId();
int getRemoteRegistrationId();
// optional uint32 localRegistrationId = 11;
boolean hasLocalRegistrationId();
int getLocalRegistrationId();
}
public static final class SessionStructure extends
com.google.protobuf.GeneratedMessage
@ -2964,6 +2972,26 @@ public final class StorageProtos {
return pendingPreKey_;
}
// optional uint32 remoteRegistrationId = 10;
public static final int REMOTEREGISTRATIONID_FIELD_NUMBER = 10;
private int remoteRegistrationId_;
public boolean hasRemoteRegistrationId() {
return ((bitField0_ & 0x00000100) == 0x00000100);
}
public int getRemoteRegistrationId() {
return remoteRegistrationId_;
}
// optional uint32 localRegistrationId = 11;
public static final int LOCALREGISTRATIONID_FIELD_NUMBER = 11;
private int localRegistrationId_;
public boolean hasLocalRegistrationId() {
return ((bitField0_ & 0x00000200) == 0x00000200);
}
public int getLocalRegistrationId() {
return localRegistrationId_;
}
private void initFields() {
sessionVersion_ = 0;
localIdentityPublic_ = com.google.protobuf.ByteString.EMPTY;
@ -2974,6 +3002,8 @@ public final class StorageProtos {
receiverChains_ = java.util.Collections.emptyList();
pendingKeyExchange_ = org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingKeyExchange.getDefaultInstance();
pendingPreKey_ = org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey.getDefaultInstance();
remoteRegistrationId_ = 0;
localRegistrationId_ = 0;
}
private byte memoizedIsInitialized = -1;
public final boolean isInitialized() {
@ -3014,6 +3044,12 @@ public final class StorageProtos {
if (((bitField0_ & 0x00000080) == 0x00000080)) {
output.writeMessage(9, pendingPreKey_);
}
if (((bitField0_ & 0x00000100) == 0x00000100)) {
output.writeUInt32(10, remoteRegistrationId_);
}
if (((bitField0_ & 0x00000200) == 0x00000200)) {
output.writeUInt32(11, localRegistrationId_);
}
getUnknownFields().writeTo(output);
}
@ -3059,6 +3095,14 @@ public final class StorageProtos {
size += com.google.protobuf.CodedOutputStream
.computeMessageSize(9, pendingPreKey_);
}
if (((bitField0_ & 0x00000100) == 0x00000100)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(10, remoteRegistrationId_);
}
if (((bitField0_ & 0x00000200) == 0x00000200)) {
size += com.google.protobuf.CodedOutputStream
.computeUInt32Size(11, localRegistrationId_);
}
size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size;
return size;
@ -3221,6 +3265,10 @@ public final class StorageProtos {
pendingPreKeyBuilder_.clear();
}
bitField0_ = (bitField0_ & ~0x00000100);
remoteRegistrationId_ = 0;
bitField0_ = (bitField0_ & ~0x00000200);
localRegistrationId_ = 0;
bitField0_ = (bitField0_ & ~0x00000400);
return this;
}
@ -3312,6 +3360,14 @@ public final class StorageProtos {
} else {
result.pendingPreKey_ = pendingPreKeyBuilder_.build();
}
if (((from_bitField0_ & 0x00000200) == 0x00000200)) {
to_bitField0_ |= 0x00000100;
}
result.remoteRegistrationId_ = remoteRegistrationId_;
if (((from_bitField0_ & 0x00000400) == 0x00000400)) {
to_bitField0_ |= 0x00000200;
}
result.localRegistrationId_ = localRegistrationId_;
result.bitField0_ = to_bitField0_;
onBuilt();
return result;
@ -3378,6 +3434,12 @@ public final class StorageProtos {
if (other.hasPendingPreKey()) {
mergePendingPreKey(other.getPendingPreKey());
}
if (other.hasRemoteRegistrationId()) {
setRemoteRegistrationId(other.getRemoteRegistrationId());
}
if (other.hasLocalRegistrationId()) {
setLocalRegistrationId(other.getLocalRegistrationId());
}
this.mergeUnknownFields(other.getUnknownFields());
return this;
}
@ -3467,6 +3529,16 @@ public final class StorageProtos {
setPendingPreKey(subBuilder.buildPartial());
break;
}
case 80: {
bitField0_ |= 0x00000200;
remoteRegistrationId_ = input.readUInt32();
break;
}
case 88: {
bitField0_ |= 0x00000400;
localRegistrationId_ = input.readUInt32();
break;
}
}
}
}
@ -4043,6 +4115,48 @@ public final class StorageProtos {
return pendingPreKeyBuilder_;
}
// optional uint32 remoteRegistrationId = 10;
private int remoteRegistrationId_ ;
public boolean hasRemoteRegistrationId() {
return ((bitField0_ & 0x00000200) == 0x00000200);
}
public int getRemoteRegistrationId() {
return remoteRegistrationId_;
}
public Builder setRemoteRegistrationId(int value) {
bitField0_ |= 0x00000200;
remoteRegistrationId_ = value;
onChanged();
return this;
}
public Builder clearRemoteRegistrationId() {
bitField0_ = (bitField0_ & ~0x00000200);
remoteRegistrationId_ = 0;
onChanged();
return this;
}
// optional uint32 localRegistrationId = 11;
private int localRegistrationId_ ;
public boolean hasLocalRegistrationId() {
return ((bitField0_ & 0x00000400) == 0x00000400);
}
public int getLocalRegistrationId() {
return localRegistrationId_;
}
public Builder setLocalRegistrationId(int value) {
bitField0_ |= 0x00000400;
localRegistrationId_ = value;
onChanged();
return this;
}
public Builder clearLocalRegistrationId() {
bitField0_ = (bitField0_ & ~0x00000400);
localRegistrationId_ = 0;
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:textsecure.SessionStructure)
}
@ -4557,7 +4671,7 @@ public final class StorageProtos {
static {
java.lang.String[] descriptorData = {
"\n\032LocalStorageProtocol.proto\022\ntextsecure" +
"\"\312\007\n\020SessionStructure\022\026\n\016sessionVersion\030" +
"\"\205\010\n\020SessionStructure\022\026\n\016sessionVersion\030" +
"\001 \001(\r\022\033\n\023localIdentityPublic\030\002 \001(\014\022\034\n\024re" +
"moteIdentityPublic\030\003 \001(\014\022\017\n\007rootKey\030\004 \001(" +
"\014\022\027\n\017previousCounter\030\005 \001(\r\0227\n\013senderChai" +
@ -4567,24 +4681,26 @@ public final class StorageProtos {
"hange\030\010 \001(\0132/.textsecure.SessionStructur" +
"e.PendingKeyExchange\022A\n\rpendingPreKey\030\t ",
"\001(\0132*.textsecure.SessionStructure.Pendin" +
"gPreKey\032\253\002\n\005Chain\022\027\n\017senderEphemeral\030\001 \001" +
"(\014\022\036\n\026senderEphemeralPrivate\030\002 \001(\014\022=\n\010ch" +
"ainKey\030\003 \001(\0132+.textsecure.SessionStructu" +
"re.Chain.ChainKey\022B\n\013messageKeys\030\004 \003(\0132-" +
".textsecure.SessionStructure.Chain.Messa" +
"geKey\032&\n\010ChainKey\022\r\n\005index\030\001 \001(\r\022\013\n\003key\030" +
"\002 \001(\014\032>\n\nMessageKey\022\r\n\005index\030\001 \001(\r\022\021\n\tci" +
"pherKey\030\002 \001(\014\022\016\n\006macKey\030\003 \001(\014\032\321\001\n\022Pendin" +
"gKeyExchange\022\020\n\010sequence\030\001 \001(\r\022\024\n\014localB",
"aseKey\030\002 \001(\014\022\033\n\023localBaseKeyPrivate\030\003 \001(" +
"\014\022\031\n\021localEphemeralKey\030\004 \001(\014\022 \n\030localEph" +
"emeralKeyPrivate\030\005 \001(\014\022\030\n\020localIdentityK" +
"ey\030\007 \001(\014\022\037\n\027localIdentityKeyPrivate\030\010 \001(" +
"\014\0322\n\rPendingPreKey\022\020\n\010preKeyId\030\001 \001(\r\022\017\n\007" +
"baseKey\030\002 \001(\014\"J\n\025PreKeyRecordStructure\022\n" +
"\n\002id\030\001 \001(\r\022\021\n\tpublicKey\030\002 \001(\014\022\022\n\nprivate" +
"Key\030\003 \001(\014B6\n%org.whispersystems.textsecu" +
"re.storageB\rStorageProtos"
"gPreKey\022\034\n\024remoteRegistrationId\030\n \001(\r\022\033\n" +
"\023localRegistrationId\030\013 \001(\r\032\253\002\n\005Chain\022\027\n\017" +
"senderEphemeral\030\001 \001(\014\022\036\n\026senderEphemeral" +
"Private\030\002 \001(\014\022=\n\010chainKey\030\003 \001(\0132+.textse" +
"cure.SessionStructure.Chain.ChainKey\022B\n\013" +
"messageKeys\030\004 \003(\0132-.textsecure.SessionSt" +
"ructure.Chain.MessageKey\032&\n\010ChainKey\022\r\n\005" +
"index\030\001 \001(\r\022\013\n\003key\030\002 \001(\014\032>\n\nMessageKey\022\r" +
"\n\005index\030\001 \001(\r\022\021\n\tcipherKey\030\002 \001(\014\022\016\n\006macK",
"ey\030\003 \001(\014\032\321\001\n\022PendingKeyExchange\022\020\n\010seque" +
"nce\030\001 \001(\r\022\024\n\014localBaseKey\030\002 \001(\014\022\033\n\023local" +
"BaseKeyPrivate\030\003 \001(\014\022\031\n\021localEphemeralKe" +
"y\030\004 \001(\014\022 \n\030localEphemeralKeyPrivate\030\005 \001(" +
"\014\022\030\n\020localIdentityKey\030\007 \001(\014\022\037\n\027localIden" +
"tityKeyPrivate\030\010 \001(\014\0322\n\rPendingPreKey\022\020\n" +
"\010preKeyId\030\001 \001(\r\022\017\n\007baseKey\030\002 \001(\014\"J\n\025PreK" +
"eyRecordStructure\022\n\n\002id\030\001 \001(\r\022\021\n\tpublicK" +
"ey\030\002 \001(\014\022\022\n\nprivateKey\030\003 \001(\014B6\n%org.whis" +
"persystems.textsecure.storageB\rStoragePr",
"otos"
};
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -4596,7 +4712,7 @@ public final class StorageProtos {
internal_static_textsecure_SessionStructure_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_textsecure_SessionStructure_descriptor,
new java.lang.String[] { "SessionVersion", "LocalIdentityPublic", "RemoteIdentityPublic", "RootKey", "PreviousCounter", "SenderChain", "ReceiverChains", "PendingKeyExchange", "PendingPreKey", },
new java.lang.String[] { "SessionVersion", "LocalIdentityPublic", "RemoteIdentityPublic", "RootKey", "PreviousCounter", "SenderChain", "ReceiverChains", "PendingKeyExchange", "PendingPreKey", "RemoteRegistrationId", "LocalRegistrationId", },
org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.class,
org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.Builder.class);
internal_static_textsecure_SessionStructure_Chain_descriptor =

View file

@ -88,6 +88,14 @@ public class Util {
dialog.show();
}
public static int generateRegistrationId() {
try {
return SecureRandom.getInstance("SHA1PRNG").nextInt(16380) + 1;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static String getSecret(int size) {
try {
byte[] secret = new byte[size];

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);
}